A client sends POST /charges to bill a customer's card. The server processes the charge successfully, but the network drops before the response reaches the client, so the client's HTTP library times out and automatically retries the identical request. How does an idempotency key prevent the customer from being charged twice?
answer
- key generated once per intent, sent on every retry
- store (key -> response) atomically with the side effect
- replay stored response instead of re-executing
- 24h-style TTL + hash the request body to catch key reuse
basics
~20 sThe client attaches a unique key to the request. The server remembers which keys it already handled and what it returned, so when the same key shows up again, it just replays the original result instead of charging the card a second time.
solid answer
~40 sThe client generates a unique idempotency key (typically a UUID) once per logical operation and sends it as a header on every attempt, including retries. On first receipt, the server processes the charge and stores the key alongside the outcome - status code and response body - in a fast, durable store, ideally within the same transaction as the charge itself. When the retried request arrives with the same key, the server looks it up, finds it already handled, and returns the stored response without re-running the charge logic. The key only works if it's generated once per user intent (not once per HTTP attempt) and if the lookup-and-store is atomic with the underlying operation to avoid a race where two retries both miss the cache and both charge the card.
go deeper
Should get that a unique key sent with the retry lets the server recognize 'I already did this' and avoid a second charge.
Should describe generating the key once per operation, storing (key, response) server-side, and replaying on retry, including the need for the store write to be atomic with the charge.
Should discuss the race condition on concurrent retries, the claim/lock pattern to close it, and a TTL policy trade-off for the dedup store.
Should raise request-body-hash validation to catch key misuse/reuse across different logical operations, and reason about failure modes when the dedup store and the business data live in different systems.
## What the key actually identifies An **idempotency key** is a client-generated, unique token that identifies a single logical operation - 'charge this customer $50 for this specific order' - independent of how many times the underlying HTTP request is physically sent. The client creates this token once, when the user's intent is formed (e.g., when the 'Pay' button is clicked), and attaches it to every network attempt for that operation, including automatic retries triggered by: - timeouts - `5xx` responses - connection resets This is the critical detail that makes the mechanism work: the key must have a lifetime tied to the operation, not to the individual request attempt, otherwise every retry would mint a fresh key and the server would see them as unrelated new charges. ## What the server does with it On the server, the idempotency key becomes a lookup into a **deduplication store** - commonly a table or a cache keyed by the idempotency key, possibly combined with a hash of the request body to detect misuse. 1. The first time a key is seen, the server executes the real business logic, then records the key together with the resulting status and response body, ideally in the same database transaction that commits the business-logic side effect. 2. When a retried request carrying the same key arrives, the server does not re-run the charge logic at all; it looks up the stored result and replays it verbatim, so the client gets the same response it would have gotten the first time. ## Why this dissolves the timeout ambiguity The reason this specifically solves the timeout ambiguity problem is that it converts a question the client cannot answer ('did my first request succeed?') into a question the server can answer deterministically ('have I seen this key before, and if so, what did I do?'). The client no longer needs to know whether the first attempt succeeded before deciding whether to retry - it can always retry, because retries are safe by construction. This is what **'idempotent'** means precisely here: applying the operation N times has the same observable effect as applying it once, even though the underlying action is not naturally idempotent on its own. ## The three trade-offs ### Where and how long the record lives The main trade-off is where and how long you store the dedup record. Storing it forever is simplest but grows unboundedly; most production systems expire idempotency keys after a bounded window - **Stripe** uses 24 hours - trading a small window of 'reused key after expiry causes a genuine second charge' risk for bounded storage. ### Atomicity with the side effect A second trade-off is atomicity: if you store the dedup key in a system separate from the one that performs the side effect, a crash between the two writes can leave you either with a charge that has no dedup record (so a retry double-charges) or a dedup record for a charge that never actually completed (so a retry never gets its money). The standard fix is one of: - write both the operation result and the idempotency record in the same **ACID transaction**; - use a **claim-based approach** - atomically insert a `'processing'` row for the key before doing the work, so a concurrent duplicate request sees the `'processing'` marker rather than racing the transaction. ### Request-body validation A third trade-off worth naming is request-body validation. If a client accidentally reuses the same idempotency key for a logically different operation, a naive dedup store will silently return the wrong cached result for the new request. Robust implementations hash the request body alongside the key and return an explicit conflict error if the same key arrives with a materially different payload, rather than silently serving a stale response for a different operation. This closes a subtle correctness hole: idempotency keys are only safe if 'same key implies same operation' actually holds, and the server should verify that invariant rather than trust the client to uphold it.
- What happens if two identical retries with the same idempotency key arrive at the server at almost the same instant, before the first one has finished writing its dedup record?Without extra protection, both requests can miss the dedup lookup and both proceed to charge the card - a race condition. The fix is to atomically claim the key first, e.g., an insert with a unique constraint that fails for the second racer, so the loser waits for or reads the winner's eventual result instead of running the charge logic itself.
- Should the idempotency key be generated by the client or by the server?It has to be generated by the client, because the entire point is to survive a case where the client never learns whether the server's response arrived - if the server generated the key and returned it in a lost response, the client would have no way to know what key to retry with. The client mints the key up front and reuses it on every retry.
- How is this different from a database UNIQUE constraint on, say, an order_id column preventing duplicate rows?A unique constraint is a special case of the same idea, but it can only prevent a duplicate write; it can't return the original response for the caller to consume, and it fails the request outright rather than transparently short-circuiting it. A full idempotency-key mechanism explicitly stores and replays the original result.
It's like writing a confirmation number on a form before mailing it to a government office, and mailing the same form three times because you never got a reply. The office recognizes the confirmation number on the second and third copies and just resends the same receipt instead of processing your application three times over.
saying these in an interview costs you the question
- Generates a new idempotency key on every retry attempt instead of reusing one per logical operation
- Stores the dedup record in a system that isn't transactionally tied to the actual side effect
- Doesn't mention any TTL/expiry strategy for the dedup store
- Assumes the idempotency key alone makes the charge operation itself idempotent, rather than the dedup-and-replay layer around it