Idempotency
Safe retries for POST — HTTP method basics, duplicate requests, idempotency keys, and handling them across servers.
Idempotency shows up in almost every API-heavy interview. Sometimes they ask for it by name. More often they ask the difference between GET, PUT, and POST — or ask you to design an idempotency handler. Same idea underneath.
What it means
Idempotency lets a client safely retry an operation without worrying about duplicate side effects.
You want the work to happen. You don’t want it to happen twice as two separate results — two cart lines for the same add, two profile rows, two $10 charges.
HTTP methods
GET, PUT, and DELETE are idempotent by nature. Call them twice; you don’t invent a second resource.
POST creates. Each successful call usually gets a new id. Retries can create twice.
GET reads, PUT updates, DELETE removes — repeating them doesn’t invent a new row.
Takeaway: only POST needs an extra idempotency design for safe retries.PUT vs POST: both change data. PUT updates an existing resource (idempotent). POST creates (not idempotent). For POST, you often make it idempotent yourself.
How duplicates happen
Client POSTs “send $10.” Without protection, that intent can charge more than once. Two common ways:
Problem A — sequential: server is slow, client times out, app retries while the first request may still be running.
First POST leaves your phone toward the server.
Sequential duplicate: first call still running when the retry arrives.Problem B — parallel: double-click / refresh — two POSTs in flight at the same time.
Same button, twice, before the UI can disable it.
Parallel duplicate: both calls look “first time” to the server.Same bad ending either way: two charges for one payment.
The fix: an idempotency key
An idempotency key is a unique id (usually a UUID) for one logical operation.
- Client generates it and puts it in the request header.
- Same operation / retry → same key. Different action → new key.
- Server stores the key so it can recognize a replay.
First time the server sees the key: save it, run the operation, return 201.
Client makes a UUID before calling the server. One payment intent → one key.
Retry the same payment → reuse this key. A different payment → new key.Key status moves: created → acquired (busy) → consumed (done). Once consumed, that key must not create new work.
Same key again
When a retry arrives with the same key:
- Status done → return 200 (already paid — don’t charge again)
- Status busy → return 409 (still working — wait)
First request finished — return success again, no new charge.
Both paths stay on screen. The highlight is which status you hit.Two requests with the same key at once can both miss the store. Put a lock on the check-and-claim path (DB row lock / mutex) so only one creates; the other follows 200 / 409 above.
Multiple servers
Local in-memory maps don’t sync across instances. Point every server at a shared key store (cache or DB).
In-memory maps don’t sync. Two servers can both accept the same payment key.
Takeaway: the key store must be shared across the fleet, not local to one process.When not to use this
Don’t put an idempotency key on every GET. Don’t use one key for a whole session — one key per logical write.
Production pitfall
Minting a new key on every retry. Same intent after a timeout → same key. Also recover keys stuck in “busy” after a crash, or retries keep getting 409.