Idempotency, Safe Methods & Retries

intermediate idempotency retries http-methods reliability api-design

Networks are flaky. Requests time out, connections drop, clients retry. If a retried “charge the customer” request runs twice, someone gets billed twice — and that’s a very bad day. Idempotency is how we make retries safe. In simple language — an idempotent operation gives the same result whether we run it once or ten times.

Safe vs idempotent — two different words

People smush these together, but they mean different things.

  • Safe — the request doesn’t change anything on the server. Pure reads. GET and HEAD are safe. We can hammer them all day and nothing on the server moves.
  • Idempotent — the request can change things, but running it again lands in the same final state. Doing it twice is no worse than doing it once.

Every safe method is automatically idempotent (reading twice changes nothing), but not every idempotent method is safe (it can still write).

GETsafe + idempotentjust reads, repeat freely
PUTidempotent"set to X" → still X after 2nd call
DELETEidempotentdeleted stays deleted
POSTNOT idempotent2 calls → 2 new resources 😱

PUT /users/5 {name:"Manish"} is idempotent — sending it five times leaves user 5 named Manish, same as sending it once. POST /users {name:"Manish"} is not — five calls create five users.

Why this matters: retries + network failures

Here’s the nasty scenario. Our client sends POST /charges. The server processes it, charges the card, and starts sending back 200 OK… but the connection dies before the response arrives.

The client has no idea what happened. Did the charge go through? It has two bad options: retry (maybe double-charge) or give up (maybe never charge). This is the classic at-least-once delivery problem — the only way to guarantee a message arrives is to be willing to send it again, which means duplicates are inevitable.

For safe/idempotent methods, retrying is fine by definition. But POST — creating things, charging cards — is exactly where retries are dangerous. That’s the gap idempotency keys fill.

Idempotency keys for POST

The trick: the client generates a unique key (a UUID) for the operation and sends it in a header. The server remembers keys it has already processed and refuses to do the work twice.

POST /charges HTTP/1.1
Idempotency-Key: 4f8c-6b2a-9d1e-7c3f
Content-Type: application/json

{ "amount": 5000, "currency": "usd", "card": "tok_visa" }

Server logic, in plain words:

  1. Look up the key. Never seen it? Do the work, store key → result, return the result.
  2. Seen it before? Skip the work entirely, return the stored result from last time.

So the client can retry the same request as many times as it wants — same key, same single charge, same response.

Same key, retried after a timeout
1Client → POST /charges, key=4f8c…
↓ server: new key → charges card, stores 4f8c→result
response lost — connection dies
↓ client didn't hear back, retries
2Client → POST /charges, key=4f8c… (same!)
↓ server: seen key → skips charge, returns stored result
Client gets result — card charged exactly once

This is exactly how Stripe’s payments API works, and it’s a very common system-design question. A few practical notes:

  • The client owns the key. It must be the same across retries of the same logical operation, and different for genuinely new ones.
  • Store keys with a TTL (say 24h) — we don’t keep them forever.
  • Watch for the in-flight race. If two retries land at the same instant, a unique DB constraint on the key (or a lock) keeps only one from doing the work.

Making retries polite

On the client side, retries should be exponential backoff with jitter — wait 1s, then 2s, then 4s, plus a little randomness so a thousand clients don’t retry in lockstep and stampede our server. And only auto-retry idempotent operations (or POSTs guarded by an idempotency key). Retrying a bare POST blindly is how double-charges happen.

Interview soundbite

Safe = no server change (GET, HEAD); idempotent = repeating it lands the same final state (GET, PUT, DELETE). POST is neither, which is a problem because networks force at-least-once retries and duplicates are inevitable. The fix is an idempotency key: the client sends a unique key per operation, the server does the work once and replays the stored result on any retry — so a lost-response retry of a payment charges the card exactly once. Pair it with exponential backoff and jitter on the client.