Idempotency keys for POST/command requests
coreintermediateGET, PUT, and DELETE are idempotent by HTTP's own definition — running the same request twice leaves the resource in the same state as running it once. POST is not: two identical POST /charges calls are, as far as the server can tell, two separate requests for two separate charges. An idempotency key is a unique value the client generates once per logical operation and sends with every retry, so the server can recognize "this is the same request again" and return the original result instead of repeating the side effect.
Think of it as
Picture a client that sends "charge this card $50" and the connection dies before the response arrives. The client has no idea whether the server never got the request, or got it, charged the card, and the response was what got lost — both look exactly like a timeout. Retrying blindly risks a second charge; not retrying risks never charging a card that was supposed to succeed. An idempotency key turns the retry into "process this exact operation, but only once" — the client can safely resend the identical request as many times as it wants, and the server's stored outcome makes every retry after the first a safe replay instead of a repeat.
What we're doing: Show a payment API using a stored idempotency key so a client's retry after a lost response returns the original charge instead of creating a second one.
- 4
- No key means no retry safety — this request is processed as a brand-new operation every time, by design (the caller opted out).
- 8
- Reusing a key with a different body is a client bug, not a duplicate — reject it rather than silently returning a mismatched stored response.
- 14
- Reserving the key before processing closes the race where two copies of the same retry arrive close together and both pass the lookup.
- 17
- The stored response is the entire point: a retry after this line returns exactly what the first, successful attempt returned — same charge_id, same status.
Why this works: The server cannot tell "the client never got my 200 OK" apart from "the client's request never arrived" — both look identical from the client's side as a timeout. Storing the outcome keyed by a client-generated idempotency key removes the ambiguity: the client can always safely retry with the same key, and the server always returns the one true result of the operation it actually performed.
No idempotency key at all — a network timeout on a successful charge leads a naive retry to double-charge the customer
Wrong
Better
What you see: A customer's statement shows two charges for one order placed during a slow network window — support has no way to explain it because, from the server's logs, both POST requests look like two entirely legitimate, independent charge attempts; nothing in the request marks them as "the same operation, retried."
Why: A POST with no idempotency key gives the server no way to distinguish a genuine second purchase from a client blindly retrying a request whose response it never saw. The fix is not "retry less" (that reintroduces the risk of silently losing a charge that failed for real) — it is giving the server the information it needs to tell the two cases apart.
- Client → API server: POST /charges, Idempotency-Key: k1
- API server → Idempotency store: lookup(k1) -> not found
- API server → API server: process charge, store outcome under k1
- API server → Client: 200 OK { charge_id: "ch_1" } (response lost in transit)
- Client → API server: timeout -> retry: POST /charges, Idempotency-Key: k1
- API server → Idempotency store: lookup(k1) -> found, outcome stored
- API server → Client: 200 OK { charge_id: "ch_1" } (replayed, no new charge)
Why POST needs an idempotency key and GET/PUT/DELETE do not
Remember: POST is not idempotent by HTTP semantics the way GET/PUT/DELETE are, so a retried POST needs its own safety net: a client-generated idempotency key, the outcome stored under that key on first processing, and every retry with the same key returning the stored outcome instead of repeating the side effect. Scope the key to its endpoint and give it an expiry — it is a short-lived retry safety net, not a permanent cache.
See also: idempotency implementation · idempotent consumer design · at most least exactly once

