Reliability
Idempotency Keys
A client-supplied key that turns 'did this already happen' from a guess into a lookup.
ReliabilityIntermediate4 min read
After this lesson, you should be able to:
- Explain what an idempotency key turns a retry into: a lookup for a stored result, instead of a guess about what already happened.
- Diagnose the failure mode idempotency keys prevent: a retried write executing twice instead of once.
- Identify the cost this mechanism adds on both sides, a client-held key and a server-stored result with an eviction policy.
- Recognize that idempotency safety applies to retries specifically, not a general guarantee that double execution is harmless.
01 What it is
An idempotency key is a value the client generates once per logical operation and attaches to every attempt at that operation, including retries. The server uses it to tell "this is the same request I already handled" apart from "this is a new one," and hands back the original result instead of running the operation again. It has to be client-supplied because only the client knows whether two calls are the same intent or two different ones. The server can't recover that from the request body alone.
02 Why it matters
Without an idempotency key, a server has no way to distinguish a retry from an independent request. That's harmless for a GET. It's not harmless for anything with a side effect: charging a card, sending an email, decrementing stock, incrementing a counter. The failure mode is concrete: a client sends a request, the connection drops before the response comes back, the client retries because that's the correct thing to do after a timeout, and the server runs the side effect a second time because nothing told it attempt two was the same operation as attempt one. The bug isn't the retry. Retrying on timeout is correct client behavior. The bug is a server with no memory of what it already did.
In Plain Terms
A coat-check attendant hands you a numbered ticket the moment you drop off your coat. If you wander off, forget whether you actually checked it, and come back to ask, you don't describe your coat and expect a new ticket. You hand over the ticket number you're already holding, and the attendant looks up that exact number to see it's the same coat from ten minutes ago, not a second coat you're suddenly bringing in. The ticket is what lets her tell "you're asking again" apart from "here's something new," and without it she'd have no way to know the difference.
03 How it works
Rendering diagram…
The store has to be checked and written before the response goes out, not after. If the server executes the operation, crashes before recording the key, and the client retries, you're back to running it twice.
04 Trade-offs
| With idempotency keys | Without them | |
|---|---|---|
| Retry safety | A retry returns the original result. The side effect runs once. | A retry re-executes the operation. The side effect can run N times. |
| Client burden | Client generates and holds onto a key per logical operation. | None. The client just calls the endpoint. |
| Server burden | Server stores a request/response record per key and needs a TTL or eviction policy for it. | None. The server is stateless with respect to retries. |
| Failure mode when skipped | N/A by design | Duplicate charges, duplicate emails, double-decremented inventory, and it happens silently, in production, under exactly the network conditions you didn't test for. |
05 Where you'll actually see this
The payment gateway case study is this mechanism end to end: the idempotency key is what lets a merchant's retry after a timeout come back with the original charge result instead of charging the card a second time.
Database replication's read-your-own-writes problem is the same root cause showing up on the other side of a request. Both are what happens when a network stops guaranteeing anything is instantaneous or exactly-once: a write might get retried, a read might land somewhere that hasn't caught up. Neither gets fixed by hoping the timing works out. Both get fixed by adding something explicit, a key to check, a place to route a read, that accounts for the delay instead of pretending it isn't there.
Message queues hit this from a third direction: a queue's at-least-once delivery means a consumer can get the same message twice, the same failure mode as a client retrying an API call, just triggered by the queue itself instead of a client giving up on a slow response.
The outbox pattern is where that third direction actually comes from in practice: its relay guarantees at-least-once by design, not as an edge case, which means a consumer reading from an outbox-backed queue needs this same defense as a matter of course, not something to bolt on if duplicates turn out to be a problem later.
How to reason about it
When asked why retries are safe here, don't just say "because it's idempotent." Name the actual mechanism: the client resends the same key, and the server looks that key up before doing anything, returning the stored result instead of re-executing. If you can't say what's being checked and where it's stored, you haven't explained why the retry is safe, you've just repeated the vocabulary.
06 Quick Check
A client retries a POST request with the same idempotency key after a timeout. What actually makes this retry safe?