Idempotency Keys: Patterns and Pitfalls
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.
The key is a claim ticket. It does not make the retry safe — it lets the server recognise the work it already did.
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
Lumi's weekly note
A short email when we publish something useful. No spam, unsubscribe anytime.