Skip to main content

Command Palette

Search for a command to run...

Idempotency Is Not the Same as Concurrency Control

Updated
•9 min read•View as Markdown
Idempotency Is Not the Same as Concurrency Control
B

I am a developer from Lagos, Nigeria who loves wealth creation and good Music. When I am not coding or thinking up ways to move forward I DANCE

Idempotency and concurrency control are distinct backend design guarantees that solve two fundamentally different failure modes in distributed systems.

Idempotency ensures that executing the same logical operation multiple times does not produce unintended additional effects beyond the first successful execution. It provides retry safety.

Concurrency control ensures that multiple operations occurring simultaneously on shared data do not create race conditions or violate application invariants. It provides race-condition safety.

Although they are often used together, they solve different problems.


1. Core Definitions & Objectives

Idempotency: Preventing Duplicate Execution

Idempotency is about operation identity; concurrency control is about overlapping access to shared state.

Idempotency guarantees that an operation can be applied repeatedly without producing additional unintended state changes beyond the initial application.

Primary Objective: Protect the system against network timeouts, retries, double-clicks, duplicated queue messages, and other situations where the same logical operation may be submitted more than once.

Core Concern:

"If I send this identical request N times because I didn't get a response, will the system charge me N times or only once?"

Primary Mechanisms:

  • Client-provided idempotency keys, for example:
X-Idempotency-Key: req_123abc
  • Storing idempotency keys and their results in a database or cache.

  • Unique constraints on database tables.

  • HTTP methods whose defined semantics are idempotent, such as GET, PUT, and DELETE.


Concurrency Control: Preventing Conflicting Execution

Concurrency control is about overlapping operations on shared state.

It manages how multiple concurrent threads, processes, services, or users read and modify the same resources.

Primary Objective: Coordinate overlapping operations on shared state to prevent concurrency anomalies and preserve application invariants.

Core Concern:

"If User A and User B both try to purchase the last available ticket at the same time, will the system sell the ticket twice?"

Primary Mechanisms:

  • Pessimistic Locking
SELECT ... FOR UPDATE;
  • Optimistic Locking

    • Version columns

    • Compare-And-Swap (CAS)

  • Distributed Locks

    • Redis-based locks

    • ZooKeeper leases

    • etcd leases

  • Database Transaction Isolation Levels

    • SERIALIZABLE

    • REPEATABLE READ

    • and other isolation strategies depending on the required guarantees


2. Practical Scenarios & Comparisons

Dimension Idempotency Concurrency Control
Problem Domain Repeated execution of the same logical operation Overlapping execution on shared state
Actor Model Often the same actor retrying an operation Different actors, or the same actor issuing parallel operations
Failure Mode Duplicate effects such as double billing or duplicate creation Race conditions such as overselling, lost updates, or corrupted totals
Scope Logical operation identity across attempts Shared-resource access across concurrent operations
Typical Identifier Idempotency key / request identity State version, row lock, resource lock, or atomic condition

Scenario A: Idempotency Without Concurrency Control

A user submits a payment for $100.

The server successfully charges the card and saves the transaction, but the HTTP connection drops before the 200 OK response reaches the user's browser.

Attempt 1

The client sends:

Idempotency-Key: pay_888

The payment is successfully processed, but the network connection drops before the client receives the response.

Attempt 2

Five seconds later, the client retries the same operation using:

Idempotency-Key: pay_888

The server checks pay_888, discovers that the operation was already completed, skips the payment processing, and returns the previously stored result.

Concurrency control was not the primary concern in this scenario because there was no overlapping execution. The problem was a sequential retry of the same logical operation.

That is an idempotency problem.


Scenario B: Concurrency Control Without Idempotency

Suppose there is only one product remaining in stock.

Two different users, User X and User Y, click Buy Now at approximately the same time.

Without proper concurrency control, something like this can happen:

User X (Req A): READ stock (1) ------> Accept purchase ------> UPDATE stock = 0

User Y (Req B): READ stock (1) ------> Accept purchase ------> UPDATE stock = 0

Both requests observed:

stock = 1

Both therefore concluded that the product was available.

The database may ultimately contain:

stock = 0

but two purchases were accepted for a single available product.

The problem is not necessarily that the stored quantity becomes -1. The problem is that the business invariant:

accepted purchases <= available inventory

has been violated.

This is a race condition.

Fix With Concurrency Control

One solution is an atomic conditional update:

UPDATE inventory
SET quantity = quantity - 1
WHERE product_id = 42
  AND quantity >= 1;

The application then verifies that exactly one row was updated.

If the first request reduces the quantity from 1 to 0, the second request's condition:

quantity >= 1

is no longer satisfied.

The second request therefore fails to reserve the product.

Another solution could use pessimistic locking:

SELECT quantity
FROM inventory
WHERE product_id = 42
FOR UPDATE;

The important point is that the shared inventory state must be protected against conflicting concurrent operations.

This scenario involves two distinct requests competing for the same resource. Neither request necessarily needs an idempotency key to solve that particular race condition.


3. The Critical Intersection: Concurrent Duplicate Requests

This is where idempotency and concurrency control intersect.

Suppose a user double-clicks a payment button.

Two identical requests containing the same idempotency key arrive almost simultaneously:

Idempotency-Key: pay_888

A naive implementation might perform:

Thread 1:
Check 'pay_888'
→ Not found
→ Process payment
→ Save 'pay_888'

Thread 2:
Check 'pay_888'
→ Not found
→ Process payment
→ Save 'pay_888'

Both threads can perform the lookup before either thread stores the key.

That creates a classic check-then-act race:

Thread 1 ──> CHECK ──> NOT FOUND ──> PROCESS PAYMENT
Thread 2 ──> CHECK ──> NOT FOUND ──> PROCESS PAYMENT

The system technically has an idempotency mechanism, but the mechanism itself is not concurrency-safe.

As a result, the payment may still be processed twice.


4. Combining Idempotency and Concurrency Control

To make the idempotency mechanism safe against concurrent duplicate requests, the system must make claiming an idempotency key atomic.

There are several ways to accomplish this.

Option 1: Database Unique Constraint

Create an idempotency record with a unique constraint on the key.

Conceptually:

CREATE TABLE idempotency_records (
    idempotency_key VARCHAR(255) UNIQUE NOT NULL,
    status VARCHAR(50) NOT NULL,
    response TEXT
);

When the request arrives, the application attempts to claim the key:

INSERT idempotency_key = pay_888

Suppose two requests attempt this concurrently.

Thread 1 → INSERT pay_888 → succeeds
Thread 2 → INSERT pay_888 → conflicts

The database's unique constraint ensures that only one record with:

pay_888

can exist.

Depending on the database and transaction structure, the second transaction may block until the first transaction resolves and then receive a uniqueness violation.

The application can then determine whether the operation is:

IN_PROGRESS

or:

COMPLETED

and either wait, retry, or return the previously stored response.

The important point is that the database performs the claim atomically.


Option 2: Distributed Mutex

Another approach is to acquire a distributed lock for the idempotency key.

For example:

Lock(
    key = "idempotency:pay_888",
    ttl = 10s
)

The first request acquires the lock and processes the operation.

A concurrent request using the same key cannot process the operation simultaneously.

Conceptually:

Incoming Request
Idempotency Key: K
        │
        ▼
Acquire Lock / Atomic DB Claim
        │
        ├───────────────┐
        │               │
   Acquired         Not Acquired
        │               │
        ▼               ▼
 Check key          Wait / Retry
        │
   ┌────┴────┐
   │         │
 Exists   Missing
   │         │
   ▼         ▼
Return     Process
stored     business
result     operation
             │
             ▼
        Save result
             │
             ▼
       Mark COMPLETE
             │
             ▼
        Release lock

The idempotency key identifies the logical operation.

The concurrency-control mechanism prevents multiple workers from claiming and executing that operation simultaneously.


5. The External Side-Effect Problem

There is another important complication in distributed systems.

Consider this sequence:

1. Create idempotency record
2. Charge payment provider
3. Save successful result
4. Mark idempotency record COMPLETE

What happens if the application crashes between steps 2 and 3?

Create idempotency record
        │
        ▼
Charge payment provider
        │
        ▼
      CRASH
        X
        │
        ▼
Result never recorded

The external payment may have succeeded, but the local application does not know whether it succeeded.

When the request is retried, blindly processing the payment again could result in another charge.

A local idempotency table alone cannot make an external side effect atomic with the application's database transaction.

This is why payment systems and other distributed workflows often combine techniques such as:

  • Downstream provider idempotency keys

  • Durable operation states such as PENDING, PROCESSING, COMPLETED, and FAILED

  • Reconciliation processes

  • Transactional messaging or outbox patterns where appropriate

The exact solution depends on where the side effect occurs and which system owns the authoritative state.


6. Summary Architectural Takeaway

The easiest way to distinguish these concepts is by the question each mechanism answers.

Idempotency asks:

"Have I already performed this logical operation?"

Its job is to prevent the same logical operation from producing duplicate effects when it is submitted multiple times.

Concurrency Control asks:

"Can these operations safely access or modify this shared state at the same time?"

Its job is to prevent overlapping operations from violating application invariants.

Their intersection asks:

"What happens when the same logical operation is executed concurrently?"

That is where an idempotency mechanism itself must be concurrency-safe.

In short:

Idempotency guarantees correctness under repeated execution by giving a logical operation a stable identity.

Concurrency control guarantees correctness under overlapping execution by controlling access to shared mutable state.

Operations exposed to both retries and concurrent access to shared state may require both mechanisms working together.