Idempotency
Making 'do it again' safe — the single idea that separates reliable distributed code from hope.
35 minDifficulty 3/5reliability · distributedReviewedRead and accepted by a person.
Before this
Why this exists
A customer taps "Pay" and the connection drops before the response comes back. Did the charge go through? Neither of you knows. They tap again. Now you have two charges — or you don't, depending on a design decision someone made months ago. Idempotency is that decision.
The mental model
A light switch that only points "on" is idempotent: flick it a hundred times, the light is on. A switch that toggles is not: the outcome depends on how many times you touched it.
How it works
The definition is about the end state, not the work
An operation is idempotent if performing it multiple times leaves the system in the same state as performing it once. Note what this does *not* say: it doesn't say the work only happens once, and it doesn't say repeats are free. `DELETE /orders/42` might hit the database every time, but after the first call the order is gone and it stays gone. That's idempotent. The second call is wasted work, not a wrong outcome.
You need it because the network can't tell you what happened
When a request times out, you learn nothing about whether it succeeded. The request might never have arrived. It might have arrived, done everything, and had its response lost on the way back. From the caller's side these are indistinguishable — and they demand opposite responses. Retry, and you risk doing it twice. Don't retry, and you risk it never happening. Idempotency dissolves the dilemma: make retrying safe, and you can always retry.
HTTP already tells you which methods promise this
GET, PUT and DELETE are defined as idempotent. POST is not, and PATCH is not guaranteed to be. This is a contract, not a suggestion — proxies, browsers and client libraries will retry idempotent methods automatically. If your `GET /reports/generate` sends an email as a side effect, something in the stack will eventually send it twice, and the bug will be reported as impossible.
For operations that can't be naturally idempotent: the idempotency key
"Charge this card £50" is inherently not idempotent — two charges is a real, different outcome. The standard fix is to have the *client* generate a unique key per logical operation and send it with the request. The server stores the key alongside the result. If a request arrives with a key it has already completed, it returns the stored result instead of doing the work again. The retry gets the original answer, and the customer is charged once.
The mechanism
The key must be claimed **atomically before** the work starts — an insert with a unique constraint, not a read followed by a write. If you check first and insert later, two concurrent retries can both pass the check and both charge the card. That race is the single most common way an idempotency implementation fails, and it only appears under load.
sequenceDiagram
participant C as Client
participant S as Server
participant D as Store
C->>S: POST /charges (key: abc-123)
S->>D: claim key abc-123
D-->>S: claimed (new)
S->>S: charge the card
S->>D: save result under abc-123
S--xC: 200 (response lost)
Note over C: timeout — did it work?
C->>S: POST /charges (key: abc-123)
S->>D: claim key abc-123
D-->>S: already exists
S-->>C: 200 + the stored resultWhat people get wrong
- Idempotent means the operation only executes once.
- It means the observable end state is the same however many times it executes. The work may genuinely repeat. What must not change is the result of it having repeated.
- Checking whether we've seen the key, then inserting it, is enough.
- That's a race. Two concurrent requests can both pass the check before either insert lands. Between your read and your write there's a window. Under retry storms, requests arrive concurrently by design, so that window gets hit.
- The server should generate the idempotency key.
- The client generates it, before the first attempt, and reuses the same key for every retry of that logical operation. A server-generated key would be different on each retry, which defeats the entire mechanism.
- POST can't be made safe to retry.
- POST isn't idempotent by definition, but an idempotency key makes a POST endpoint safe to retry. The method's semantics and your endpoint's behaviour are separate things. The key adds the guarantee HTTP doesn't.
When not to use it
- The operation is naturally expressible as "set to this value".
- A PUT with the full desired state. You get idempotency for free with no key infrastructure.
- You need to guarantee the work happens exactly once across a distributed system, not just that the end state is right.
- Transactional outbox or a distributed transaction. Exactly-once execution is a much stronger and more expensive guarantee than idempotency.
Terms
- Idempotency key
- — A client-generated unique identifier for one logical operation, reused across every retry of it.
- Safe method
- — An HTTP method that doesn't modify state at all — GET and HEAD. Every safe method is idempotent; not every idempotent method is safe.
- Exactly-once
- — A guarantee that work executes precisely one time. Much harder than idempotency, and usually not what you actually need.
- Natural key
- — An identifier already present in the data, like an order id, that can serve as the dedupe key without inventing one.
In an interview
How would you make a payment endpoint safe to retry?
- client generates an idempotency key before the first attempt and reuses it on retries
- server claims the key atomically before doing the work — unique constraint, not check-then-insert
- store the response and replay it for repeat requests with the same key
- expire keys after a sensible window, typically 24 hours
Is PUT or POST the right method for creating a resource?
- PUT when the client knows the id and is stating the desired state — idempotent by definition
- POST when the server assigns the id — not idempotent without an explicit key
- the choice determines whether retries are safe by default
Can you recall it?
Why does a timeout force you to think about idempotency at all — and what does an idempotency key change?
Connected ideas
- Webhooks — Inverting the call — they tell you when something happened instead of you asking forever.
- Message Queues — Hand the work to someone else and answer now — the backbone of every responsive backend.
- Transactions & ACID — All or nothing — plus the isolation levels that decide which anomalies you're allowing.
- Race Conditions — Two things interleaving in an order you never tested — the bug class that only shows up in production.
Also part of
This idea matters in more than one area — which is usually why it matters.