THIS EXPLANATION
THE ROOM
CDA·114 Computing, Data & AI 7 MIN · 8 STATIONS

Idempotent operations

A Socratic walk-through of idempotent operations — reasoned out one step at a time, not lectured.

abcdefgh
a

The question we started with

THE QUESTION #

Why must an action that might be retried be designed for it rather than simply repeated?

You send a payment request. The network hesitates. No reply comes back. Do you send it again?

Notice how uncomfortable that question is, and notice exactly why it is uncomfortable. It is not that you do not know what happened — it is that you cannot know, and no amount of waiting will tell you. The silence is identical whether the request never arrived, or arrived and was processed and the acknowledgement was lost on the way home. So the retry is a gamble: it either fixes a lost request or creates a duplicate charge.

That is the whole subject. The interesting move is not "retry carefully." It is to change the operation so that the gamble no longer has two outcomes.

b

Reasoning it through

REASONING #

Let us be precise about the impossibility first, because everything else follows from it. This is the Two Generals Problem, and it is genuinely unsolvable, not merely hard: over an unreliable channel, no finite exchange of messages can give both parties certainty that the other acted. Adding an acknowledgement to the acknowledgement just moves the uncertainty to the last message. So a caller facing silence has exactly two choices — give up, or retry — and neither is safe on its own.

Now ask a different question. What property would an operation need, so that retrying it were harmless? Say it plainly: applying it twice must leave the world in the same state as applying it once. That property has a name borrowed from mathematics — idempotence, from idem (same) and potens (power). An operation f is idempotent when f(f(x)) = f(x).

Test that against real operations and watch which pass. "Set the shipping address to 14 Elm Street" — apply it twice, same result. Passes. "Delete order 8812" — the second attempt finds nothing to delete and the world is unchanged. Passes, provided you agree that a second delete reporting "already gone" is success rather than an error. "Add 50 euros to the balance" — fails, plainly. "Append this line to the log" — fails.

Do you see the pattern? Operations that declare a desired state are naturally idempotent. Operations that describe an increment or an event are not. This is exactly why HTTP defines PUT and DELETE as idempotent and POST as not: PUT says "make the resource be this", POST says "here is one more thing." The distinction is not arbitrary convention, it is the same mathematical property showing up in a protocol.

Which brings the awkward case. Most operations that matter commercially are increments and events: charge a card, ship a parcel, send an email, publish a message. You cannot restate them as desired states without losing meaning — "the customer's total charges should be 50 euros" is not what a payment means. So what can you do?

You can give the operation an identity. The caller generates a unique key — a UUID, a client-side order reference — and attaches it to the request. The server records, atomically with performing the effect, that this key has been applied and what the result was. A second request bearing the same key is not performed again; the stored result is returned instead. The operation itself is still an increment, but the request has become idempotent, which is all the caller needed. This is what Stripe's Idempotency-Key header does, and what the deduplication identifiers in message brokers do.

Two details make or break it, and both are easy to get wrong. The key and the effect must be committed together — if you apply the charge and then crash before recording the key, the retry charges again, and you have merely narrowed the window rather than closed it. And the record must live at least as long as the caller might retry, which in practice means hours or days, not the lifetime of a process's memory.

One more honest point. People describe this as achieving "exactly-once delivery." It is not delivery that becomes exactly-once — messages are still delivered many times or none. What becomes exactly-once is the effect, which is why the careful literature says effectively-once. Delivery guarantees come in two real flavours, at-most-once and at-least-once, and idempotence is how you build something that behaves like the third out of the second.

c

The analogy

THE ANALOGY #
THE FIGURE

Think of a lift call button. You press it; nothing visibly happens; you press it four more times. The lift does not arrive five times, and five lifts are not dispatched. The button records a state — "someone on this floor wants to go up" — and repeated presses simply reassert a state that is already true. That is idempotence you use without noticing, and it is why the button is designed as a request for a condition rather than as an order for one journey.

WHERE IT BREAKS DOWN

the lift button forgets its state the moment the doors open, whereas an idempotent service must remember which requests it has already honoured, for as long as any caller might retry — and that memory, not the button-pressing, is where the real engineering lives.

d

Clarifying the model

THE MODEL #

A few refinements that catch people out.

Idempotence is not the same as being read-only. A safe operation changes nothing; an idempotent one may change a great deal, but converges to the same state however many times it is applied. Conversely, an operation can be non-idempotent while looking harmless — an "increment the view counter" endpoint has no obvious risk until a retry storm inflates your numbers.

It is also not the same as commutativity. Idempotence says a repeat is harmless; it says nothing about two different operations arriving out of order. "Set the address to A" and "set the address to B" are each idempotent, and applying them in the wrong order still leaves the wrong address. Retries and reordering are separate problems and want separate defences — often a version number or a conditional update.

And notice which end of the wire bears the cost. The caller cannot make an operation idempotent by retrying it politely; only the receiver can, by defining the operation as a state assertion or by keeping the key ledger. This is why "design for retry" is a contract, not a client-side habit — a client that retries against a service which never promised idempotence is doing something unsafe, however careful its backoff.

e

A picture of it

THE PICTURE #
Idempotent operations
Idempotent operations Enter at the slanted input and go to the diamond, the only question the design turns on. The first-time branch performs the effect and writes the key in a single commit -- that single commit is the load-bearing detail, not the lookup. The retry branch is answered from the store with no new effect. The third branch is the honest hazard: with no usable key, a retry is indistinguishable from a new request, and the red terminal is a duplicate charge. {"generator":"mermaid-svg-renderer@3.2.1","source":"../Socrates/.diagram-cache/_src/idempotent-operations.md","sourceIndex":1,"sourceLine":4,"sourceHash":"100232753438f3f5d01f99927b25effcbddc31fccfe1376b1c828f0fb2308f2b","diagramType":"flowchart-v2","layoutVariant":"source","repairedDuplicateIds":[],"motion":"entrance-with-reduced-motion-fallback","presentation":"editorial","attempt":1,"viewBox":{"x":0,"y":0,"width":1203,"height":736},"qa":{"passed":true,"findings":[]}} consulted yes, this is a retry no, first time seen cannot tell key and result committedtogether Request arrives carrying anidempotency key Has this key been appliedbefore? Ledger of applied keys and theirresults Perform the effect and recordthe key in one commit Return the stored result withouttouching anything No key, or the ledger hasforgotten it The effect happens twice Caller sees one outcomehowever often it asked
KINDSsourcedecisionreferenceprocessoutcomerisk

How to readEnter at the slanted input and go to the diamond, the only question the design turns on. The first-time branch performs the effect and writes the key in a single commit — that single commit is the load-bearing detail, not the lookup. The retry branch is answered from the store with no new effect. The third branch is the honest hazard: with no usable key, a retry is indistinguishable from a new request, and the red terminal is a duplicate charge.

f

What became clearer

WHAT CLEARED #
WHAT CLEARED

The uncertainty a retry lives with cannot be removed — no protocol can tell a lost request from a lost reply. What can be removed is the consequence of guessing wrong. Restate an operation as a desired state where you can, and where you cannot, give the request an identity and commit that identity alongside its effect. Then retrying stops being a gamble and becomes ordinary, which is why a service that expects to be retried must be built for it deliberately rather than hoping callers are gentle.

g

Where to go next

ONWARD #
  • How exponential backoff with jitter keeps safe retries from becoming a retry storm.
  • What message brokers actually promise, and how their deduplication windows expire.
h

Key terms

TERMS #
TermWhat it means
Idempotencethe property that applying an operation more than once leaves the same state as applying it once.
Idempotency keya caller-generated unique identifier attached to a request so the receiver can recognise a retry of it.
Two Generals Problemthe proof that two parties communicating over an unreliable channel cannot both become certain the other acted.
Effectively-oncethe accurate name for the guarantee usually sold as exactly-once: delivery repeats, but the effect happens once.

Every term the collection defines is gathered in the glossary.

Nearby on the shelf

4