Skip to content
Discovery Distributed Systems 3 min read · Updated 4 Aug 2026

Idempotency Keys: Patterns and Pitfalls

intermediate apisreliabilitydistributed-systems

The network gives a client no way to tell a lost request from a lost response. When a call times out, the only thing the caller knows is that it did not hear back — not whether the payment was taken. Its only safe move is to try again, and the server’s job is to make that second attempt harmless.

An idempotency key is how. The client generates a unique token, sends it with the request, and reuses that same token on every retry of the same logical operation. The server uses it to recognise the repeat.

Request, retry and replay through an idempotency store A first POST with an idempotency key passes through the gateway to the payments API, misses the store, executes the business logic, commits to the database and stores the key with its response, returning 201 Created. A retry with the same key hits the store and replays the same 201 without reaching the business logic or the database. A retry with the same key but a different payload returns 409 Conflict. Client client retries Gateway forwards Payments API checks key Idempotency store key · hash · reply Business logic charges once Database commits 1 2 MISS 3 stores response + 201 same key, retried HIT → replays 201 Created logic and database untouched 409 Conflict same key, different payload payload hash does not match POST /payments Idempotency-Key: 01HW9Z…

The key is a claim ticket. It does not make the retry safe — it lets the server recognise the work it already did.

Request, retry and replay. The first request misses the store, runs the business logic and commits — then writes the key and its response back. The retry takes the same route, hits the store, and replays the original answer: the business logic and the database are never reached. A third path shows the conflict case, where the key is reused with a different payload.

The diagram carries the whole idea, and it is worth reading before the prose: the second pass stops early. That is the property you are buying.

Which operations need a key

GET, PUT and DELETE are already idempotent by definition — the same PUT twice leaves the same state. POST is not, and neither is anything that increments, appends, charges or sends. Those are the operations that need a key.

Put it in a header rather than the body, so it survives independently of any change to the payload:

POST /payments HTTP/1.1
Idempotency-Key: 01HW9ZJNBY6MWQ2FX7A5K3J0B9
Content-Type: application/json

{ "amount": 1999, "currency": "USD" }

Bind the key to the payload

The subtle failure is a client reusing a key with a different body — usually a bug, occasionally an attack. Store a hash of the request payload alongside the key. If the key returns with a payload that does not match, respond 409 Conflict rather than either replaying the old result or performing the new operation. Both of those silently do the wrong thing.

That is the amber branch in the diagram, and it is the one most implementations leave out.

Store the result, not just the key

Recording “I have seen this key” is not enough. The retry needs the same response the first attempt produced, including its status code. Store the response body and status against the key and return them verbatim on a repeat.

Records need a TTL. A few days is usually right: long enough to cover any plausible retry window, short enough that the table does not grow forever. Redis is a common home for this precisely because expiry is built in and the lookup sits on the hot path of every write.

Concurrency is the hard part

Two retries can arrive simultaneously. If both check the key, both find nothing, and both proceed, the whole mechanism has bought you nothing.

The check and the claim must be atomic — a unique constraint on the key column, or an atomic set-if-absent in your key-value store. Never a read followed by a write. This is the single most common way a correct-looking implementation turns out not to be one under load.

What you have actually built

Not exactly-once delivery — that does not exist across a network. What you have is at-least-once delivery plus idempotent handling, which produces exactly-once effects. Since effects are the only thing a user can observe, that is the property that was worth having all along.

Quick answers

What is an idempotency key?
A unique token a client generates and sends with a request, reusing the same value on every retry of that same logical operation. The server uses it to recognise a repeat and replay the original response instead of performing the work twice.
Where should the idempotency key go?
In a request header rather than the body, so it survives independently of any change to the payload. Idempotency-Key is the conventional name and the one Stripe and others use.
How long should idempotency records be kept?
Usually a few days — long enough to cover any plausible retry window, short enough that the table does not grow without bound. Redis is a common home because expiry is built in and the lookup sits on the write path.
What happens if the same key arrives with a different payload?
Return 409 Conflict. Store a hash of the request alongside the key and compare it; replaying the old response or performing the new operation both silently do the wrong thing.

References

Related Discoveries