How do I design idempotent APIs?
An API is idempotent when making the same request twice has the same effect as making it once. That matters because the network gives you no way to tell a lost request apart from a lost response — the client that times out cannot know whether the work happened, so its only safe move is to retry.1
The standard mechanism is an idempotency key: a unique token the client generates, sends with the request, and reuses on every retry of that same logical operation.
Where keys are needed
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 the key in a header rather than the body, so it survives independently of payload changes:
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.1
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 it verbatim on a repeat.
A retry that gets a 200 the first time and a 409 the second has not been
made idempotent; it has been made confusing.
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 exactly because expiry is built in and the lookup is on the hot path of every write.2
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.2
Idempotency is not exactly-once
Exactly-once delivery does not exist across a network. What you can build is at-least-once delivery plus idempotent handling, which produces exactly-once effects — and that is the property that actually matters to a user. Design for the retry rather than trying to prevent it.
You might want to explore
Lumi's weekly note
A short email when we publish something useful. No spam, unsubscribe anytime.