DevTools Hub

Search tools

Search for a developer tool

Idempotency Explained

Part of the Distributed Systems Toolkit

"Idempotent" gets used as a synonym for "has no side effects" often enough that it's worth stating the actual definition precisely, because the common version is wrong in a way that matters: an idempotent operation can absolutely have side effects. DELETE /orders/42 deletes an order — that's a real, permanent side effect — and it's still idempotent. What idempotency actually guarantees is narrower and more specific: calling an operation once produces the same end state as calling it any number of times. The side effect happens; it just doesn't accumulate.

The definition, precisely

Compare two operations on a bank balance: "set balance to $100" and "add $100 to balance." The first is idempotent — call it once or fifty times, the balance ends up at exactly $100 either way. The second is not — call it fifty times and the balance is $5,000 higher than it should be, an error that gets worse with every retry. Both operations have a side effect. Only one of them is safe to repeat.

There's a second, subtler trap: idempotency is about the operation's effect, not necessarily its response. A DELETE on a resource that exists returns 200 or 204; the identical DELETE sent again — the resource is already gone — returns 404. Different status code, same end state (the resource doesn't exist). That's still idempotent, precisely because idempotency was never a promise about the response matching — only about the state the system converges to.

HTTP methods: which ones actually are, and why

RFC 9110 (2022, obsoleting the older RFC 7231) defines this explicitly per method, and the reasoning behind each one is worth knowing rather than memorizing:

MethodIdempotent?Why
GET / HEADYesRead-only — no state change at all, so repetition is trivially safe
PUTYes"Replace this resource with exactly this representation" — the end state is the request body, regardless of how many times it's applied
DELETEYes"Ensure this resource doesn't exist" — already satisfied on the second call
POSTNo"Create a new resource" / process per resource-specific semantics — retrying typically creates a second one
PATCHDependsNot guaranteed by spec — a JSON Merge Patch ("set field to X") is naturally idempotent; a JSON Patch "add to array" operation is not, since each retry appends again

One caveat worth being explicit about: this table describes a contract, not a law of physics. Nothing stops a server from implementing a GET endpoint that increments a view counter on every call — a real, common violation of the spec that works fine right up until something downstream (a proxy, a client, a retry policy) assumes the contract holds and safely repeats a call it shouldn't have.

Why this matters: the client can never be sure a request landed

This is the same unavoidable fact Message Queues Explained opens with, applied to a single request instead of a queue: a client that times out waiting for a response cannot tell "the request never arrived" from "the request was processed but the response was lost." If the operation is idempotent, the safe move is obvious — retry it, worst case it runs again with no additional effect. If it's not idempotent, that same blind retry can double-charge a card or create a duplicate order. This is exactly why browsers warn before resubmitting a form on page refresh — the browser knows the last request was a POST, and a POST is exactly the method with no built-in guarantee that repeating it is safe.

Making a non-idempotent operation safe to retry: the idempotency key

POST can't be made idempotent by definition — creating a resource is inherently a "do this again" operation. The practical fix, used by Stripe and PayPal's payment APIs among others, is a client-generated idempotency key: a unique ID attached to one logical operation (not regenerated on retry), sent as a header. The server checks whether it has already processed that exact key; if so, it returns the stored result from the first execution without repeating the underlying work at all.

Attempt 1
Idempotency-Key: abc123
Look up key abc123not found
Charge card $50executed
Store result under abc123
Client times out before the response arrives — did the charge happen? No way to tell from here.
Attempt 2 (retry)
Idempotency-Key: abc123
Look up key abc123found
Charge card $50skipped
Return the stored result
Same response as attempt 1, card charged exactly once.

The key — not the request body — is what's deduplicated on, which is what turns a blind retry of a non-idempotent operation into a safe one.

This isn't free or permanent: Stripe retains idempotency keys for 24 hours, after which the same key is treated as a new request. An idempotency key is a bounded safety window for handling retries around one uncertain network round trip, not a permanent record of every operation a client has ever attempted.

The same problem, on the consumer side of a queue

Message Queues Explained covers why at-least-once delivery requires an idempotent consumer — a message can be redelivered after a visibility timeout expires even though it was already fully processed. The mechanisms are the same two options as above, just applied to message processing instead of an HTTP request:

  • Natural idempotency — some operations are idempotent for free, because of how they're structured, with no extra machinery required. A database UPSERT keyed by the message's ID, adding an element to a set, or setting a field to an absolute value are all naturally idempotent. Appending to a list or incrementing a counter are not — processing the same message twice visibly changes the result a second time.
  • Enforced idempotency via a dedup store — for anything not naturally idempotent, track which message IDs have already been fully processed, checked before doing the real work. The trap here is subtle: the check and the mark-as-done have to be atomic with the actual state change, not two separate steps — a crash between "checked, not yet processed" and "marked done" reopens exactly the race the dedup store existed to close.

Either way, the ID used for dedup has to be assigned once, by the producer, and stay stable across every redelivery of that logical message — never derived by the consumer from arrival time or a hash of contents that might legitimately repeat.

Try it yourself

Distributed ID Explorer generates the kind of ID an idempotency key or a message dedup key actually needs — unique, collision-resistant, and (with a ULID, KSUID, or Snowflake ID) sortable by creation time, which makes it easy to expire old keys out of a dedup store on a rolling window instead of keeping them forever.

FAQ

Does idempotent mean safe to call concurrently?

No — that's a genuinely different guarantee. Idempotency describes what happens across repeated sequential calls; it says nothing about two in-flight calls racing each other before either has finished. Two simultaneous PUT requests can still interleave badly depending on how the server implements the write — idempotency and concurrency-safety (atomicity) are separate properties that happen to both matter for the same "is this safe to retry" question.

Is PATCH idempotent?

It depends entirely on what the patch document actually describes, which is exactly why it's the one method the spec doesn't give a flat answer for. "Set the email field to this value" is idempotent — repeating it changes nothing further. "Append this item to the tags array" is not — every retry adds another one.

Why does my browser warn about resubmitting a form on refresh?

Because the browser tracked that the page was the result of a POST, and refreshing would resend that exact non-idempotent request — the same order, comment, or payment submitted again. It's the browser applying the same caution this whole post is about: never blindly repeat something with no guarantee that repeating it is safe.

Related tools