Skip to content

Idempotency

Recall the irreducible ambiguity from the overview: when a request times out, you cannot tell whether it ran. The only sane response is to retry — but a retry of a request that did run creates a duplicate, and a duplicate “charge the customer” or “ship the order” is a catastrophe. Idempotency is the discipline that breaks this trap: design operations so that doing them twice has the same effect as doing them once. Then duplicates stop being dangerous, and retries become free. This page is about why the network forces this on you, and how to build it.

The forcing function: the network delivers duplicates

Section titled “The forcing function: the network delivers duplicates”

It’s worth saying plainly, because it reframes idempotency from a nice-to-have into a structural requirement: you do not get to choose whether duplicates happen. They happen because:

Client ──"charge $50"──► Server [server charges, sends OK]
reply lost in network ◄┘
Client times out, doesn't know it worked, RETRIES
Client ──"charge $50"──► Server [server charges AGAIN — $100!]

The lost reply is indistinguishable from a lost request (see Timeouts, Retries & Backoff). Any reliable client must retry, so any robust server must tolerate the duplicate. The duplicate is not a bug to prevent; it’s a condition to absorb.

An operation is idempotent if applying it multiple times yields the same result as applying it once. The word comes from math (a function f where f(f(x)) = f(x)), but the systems definition is about observable effect on state, not return value.

Some operations are naturally idempotent; some are not:

OperationIdempotent?Why
SET balance = 100YesRe-running lands on the same state
balance = balance + 50NoEach run adds again
HTTP GET, PUT, DELETEYes (by spec)Same target state regardless of count
HTTP POSTNo (by spec)“Create a new resource” each time
DELETE WHERE id = 7YesAfter the first, it’s already gone

Idempotency keys: making anything idempotent

Section titled “Idempotency keys: making anything idempotent”

The non-idempotent operations are the dangerous ones (POST /charges, “place order”), and you can’t always rewrite them into naturally-idempotent form. The general solution is the idempotency key: a unique token the client generates for a logical operation and attaches to every retry of it. The server uses the key to recognize and collapse duplicates.

Client generates key once: K = "a1b2-c3d4" (e.g. a UUID)
Attempt 1: POST /charges Idempotency-Key: a1b2-c3d4 → (lost reply)
Attempt 2: POST /charges Idempotency-Key: a1b2-c3d4 → server: "I've
seen K, here's
the SAME result"
The customer is charged exactly once, no matter how many retries.

The server-side logic, conceptually:

on request with key K:
look up K in a dedup store
├─ not seen: process it, atomically store (K → result), return result
├─ seen, done: return the STORED result (don't re-run the side effect)
└─ seen, in-flight: reject or wait (a retry arrived before the first finished)

Two details make or break correctness. First, the key must be generated by the client, once per logical operation, and reused across retries — if the client makes a fresh key per attempt, the server sees them as different requests and the protection vanishes. Second, storing the result and performing the side effect must be atomic (or at least the side effect must itself be idempotent under the key), or a crash between the two reintroduces the duplicate. This is why payment APIs like Stripe make Idempotency-Key a first-class header.

The same pattern appears in messaging. Most queues guarantee at-least-once delivery — they’d rather deliver a message twice than lose it — so consumers receive duplicates routinely. The consumer defends itself by deduplicating on a message ID:

consumer receives message with id M:
if M in processed-set: skip (already handled)
else: process M, then record M as processed

This is idempotency wearing different clothes: the message ID is the idempotency key. The closely related (and frequently misunderstood) goal of making the whole pipeline behave as if each message took effect once — even across failures — is exactly-once semantics, which is built from at-least-once delivery plus idempotent processing. We unpack why true exactly-once delivery is impossible but exactly-once effect is achievable in Exactly-Once Semantics.

Idempotency buys you the freedom to retry without fear — the single most important property for building reliable systems on an unreliable network. It converts the network’s worst habit (delivering the same thing twice) into a non-event, and it’s what makes timeouts, retries, queues, and failover safe to use at all. The cost is real but modest: you must thread idempotency keys through your APIs, maintain a dedup store (with a retention window and the storage that implies), and reason carefully about the atomicity of “do the thing and record that you did it.” That bookkeeping is the price of admission to a distributed system that doesn’t double-charge its customers — and it is always cheaper than the alternative.

Idempotency is less a feature you choose than a tax the network levies — run it through the five questions anyway, since how you implement it is a real choice:

  • Why does it exist? Because the network forces it: a timed-out request is indistinguishable from a lost one, so any reliable client must retry — and a retry of something that already ran creates a duplicate (a $50 charge becomes $100). Idempotency makes doing an operation twice equal doing it once.
  • What problem does it solve? It turns the network’s worst habit — delivering the same thing twice — into a non-event, which is what makes retries, timeouts, queues, and failover safe to use at all.
  • What are the trade-offs? Some operations are naturally idempotent (SET, PUT, DELETE); the dangerous ones (POST /charges, “place order”) aren’t, so you thread a client-generated idempotency key through your APIs and maintain a dedup store sized by request rate × retention window (Stripe keeps keys 24 h, ~17 GB at 1,000 charges/s). The side effect and recording the key must be atomic.
  • When should I avoid it? Skip the key machinery for naturally-idempotent operations — they need no bookkeeping. And tune the retention window deliberately: too short and a late retry slips through as “new,” too long and the dedup store grows without bound.
  • What breaks if I remove it? A dropped response plus a blind retry double-charges the customer or double-ships the order, and an at-least-once queue’s redelivery gets processed twice. The whole reliability stack — aggressive retries with backoff — turns from a feature into a hazard.
  1. Why does the ambiguity of a timed-out request force both retries and idempotency, rather than either being optional?
  2. Classify these as idempotent or not, and say why: SET x = 5, x = x + 1, HTTP PUT, HTTP POST.
  3. Why must an idempotency key be generated by the client once per logical operation and reused on retries, rather than freshly per attempt?
  4. Why must “perform the side effect” and “record the idempotency key/result” be atomic? What goes wrong if a crash falls between them?
  5. How is consumer-side deduplication in a message queue the same idea as an idempotency key, and how does it relate to exactly-once semantics?
Show answers
  1. A timed-out request is indistinguishable from a lost request — you can’t tell whether it ran — so any reliable client must retry. But a retry of something that did run creates a duplicate, so any robust server must tolerate it. The ambiguity forces both: retries are non-optional, and the duplicates they create make idempotency non-optional too.
  2. SET x = 5 is idempotent (re-running lands on the same state); x = x + 1 is not (each run adds again); HTTP PUT is idempotent (same target state regardless of count); HTTP POST is not (it means “create a new resource” each time). What’s invariant for the idempotent ones is the resulting state, not the return value.
  3. The key identifies a logical operation, and every retry of that operation must carry the same key so the server recognizes them as the same request and collapses the duplicate. If the client mints a fresh key per attempt, the server sees each retry as a distinct request and the protection vanishes entirely.
  4. Performing the side effect and recording the key/result must be atomic so the server never does one without the other. If a crash falls between them — side effect done, key not yet recorded — a retry finds no record, re-runs the side effect, and reintroduces the duplicate (e.g. a double charge).
  5. The message ID is the idempotency key: the consumer records processed IDs and skips any it has already handled, exactly as a server collapses duplicate keys. Queues guarantee at-least-once delivery, so duplicates are routine; exactly-once semantics is built from at-least-once delivery plus this idempotent processing — exactly-once effect, not exactly-once delivery (see Exactly-Once Semantics).