Skip to content

Concept

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.

What happens if you retry?
GET / PUT / DELETE → one result
Send the same request two times…GET · PUT · DELETE✓ same end statesafe to retryPOST✗ may create twicenot safe by defaultIdempotent by nature — retries are fine

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.

Problem A — timeout, then retry
Step 1 — You pay $10
One “Pay $10” click · requests arrive one after anotherYoupay $10ServerBankWatch the request path left → right

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.

Problem B — two requests at once
Step 1 — Double-click Pay
One “Pay $10” intent · two requests in the same momentYouPayPOST A$10POST B$10BankSame end problem as the timeout case — different cause

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.

  1. Client generates it and puts it in the request header.
  2. Same operation / retry → same key. Different action → new key.
  3. Server stores the key so it can recognize a replay.

First time the server sees the key: save it, run the operation, return 201.

Solution — first request with a key
Step 1 — Create a unique key
Happy path: this key has never been seen beforeClientnew key…ServerKey storelookupKey names this payment — not the product

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: createdacquired (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)
Same key comes back — what do we return?
Key status: DONE → 200
Retry uses the same key. Server only checks status.Retrysame keyStatus = done (consumed)→ HTTP 200 · “already paid”Status = busy (in progress)→ HTTP 409 · “wait, don’t charge again”Don’t create a second payment.

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).

Many servers — where do keys live?
BAD — each server has its own map
Same payment key can hit Server A or Server BServer AServer BServer Clocal maplocal maplocal mapBAD: both think “new key”→ two charges across instances

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.