The definition, stated carefully
An operation is idempotent when N identical requests leave the server in the same state as one request.
Every word of that is doing work, and the load-bearing phrase is state. Idempotency is a claim about what the system holds after the requests, and about nothing else. In particular:
It is not a claim that the responses are identical. This is the misconception that causes the most confusion, and it is worth stating in the negative because so many summaries assert the opposite. Two identical requests to an idempotent endpoint may return different status codes, different bodies and different headers, and the endpoint remains perfectly idempotent as long as the stored state converges.
It is not a claim about concurrency. Idempotency says what happens when a request is
repeated, not what happens when two different requests race. Two clients each sending an idempotent
PUT with different bodies still produce a last-writer-wins outcome, and one of them loses an
update. That is a separate problem solved by preconditions such as If-Match and a
412 response.
It is not a claim about observability. A second call may well write an access log line, increment a metric or count against a rate limit. Those are effects, but they are not the resource state the operation is defined over, and no useful definition would exclude them, because then nothing at all would be idempotent.
The example that settles it: DELETE
DELETE is the cleanest demonstration, and if you can explain this you understand the concept.
DELETE /v1/orders/ord_9f2c41 → 204 No Content (the order existed; now it does not) DELETE /v1/orders/ord_9f2c41 → 404 Not Found (there is nothing there) DELETE /v1/orders/ord_9f2c41 → 404 Not Found (still nothing there)
The responses are not the same. A 204 and a 404 are
about as different as two successful-looking outcomes get, and a client comparing response bodies would
conclude something changed between calls.
Look at the state instead. After the first request, the order does not exist. After the third, the order
does not exist. After the thousandth, the order does not exist. The server converged after call one and
has not moved since. DELETE is idempotent, exactly as RFC 9110 says, and the
differing status codes are irrelevant to that fact — they are the server accurately reporting what it
found, which is a separate and good thing for it to do.
The converse case is just as instructive. Imagine an endpoint that returns a byte-identical
200 OK every single time, and appends a row to an audit table on each call. Identical
responses; not remotely idempotent. Response equality is neither necessary nor sufficient.
Safe, idempotent, neither
HTTP defines two related properties, and they nest rather than overlap.
- Safe — the request is not intended to change state at all. It is a read.
- Idempotent — repeating it has no effect beyond the first application.
Every safe method is idempotent, because performing no change several times is still no change. The
implication does not run backwards: PUT and DELETE change state substantially
and are idempotent anyway. So the useful mental model is three tiers, not two.
| Method | Safe | Idempotent | Why |
|---|---|---|---|
GET | Yes | Yes | A read; changes nothing to repeat |
HEAD | Yes | Yes | A read with the body suppressed |
OPTIONS | Yes | Yes | Asks what is permitted |
TRACE | Yes | Yes | Echoes the request back |
PUT | No | Yes | Assigns a complete state; assigning it twice is assigning it once |
DELETE | No | Yes | Converges on absence after the first call |
POST | No | No | Accumulates — each call adds another member or event |
PATCH | No | No | A patch document may express a relative change |
Two caveats on that table. First, these are properties of the method as specified, not promises
about your server: an endpoint can be written to make GET delete things, and the protocol
cannot stop it — it simply means every cache, crawler and prefetcher on the path is now dangerous to you.
Second, PATCH is listed as non-idempotent because the specification permits relative
instructions, but individual patch documents that only assign fixed values are idempotent in practice;
PUT vs PATCH works through which is which. The
HTTP methods reference covers all nine methods and their cacheability
alongside these two properties.
Why any of this matters: the unknown outcome
Idempotency would be trivia if networks were reliable. The reason it is one of the load-bearing ideas in distributed systems is a single unavoidable fact:
When a request fails, the client cannot tell whether the server performed the operation.
A timeout is compatible with at least four different histories. The request never arrived. The request arrived and the server crashed before acting. The server acted and then crashed before responding. The server acted, responded successfully, and the response was lost on the way back. From the client's position all four look identical, and no amount of additional waiting distinguishes them.
So the client faces a genuine dilemma with no protocol-level answer. Retry, and if the operation already happened it happens twice. Do not retry, and if it never happened the user's action is silently lost. Both failure modes are real, and picking one arbitrarily is how systems end up with either duplicate charges or vanished orders.
Idempotency dissolves the dilemma. If repeating the operation cannot make things worse, the client simply retries and stops caring which history it is in. That is the whole value proposition, and it is why the property is worth engineering for rather than merely documenting.
This is also why HTTP infrastructure you did not configure will retry idempotent methods on your behalf.
Client libraries, connection pools, load balancers and service meshes commonly resend a failed
GET or PUT automatically and refuse to resend a POST. The behaviour
follows the specification, which means an endpoint whose real semantics disagree with its method will be
retried according to the method, not according to what it actually does.
Delivery semantics: at-least-once, at-most-once, exactly-once
The same problem shows up wherever messages cross a network, and the vocabulary is worth having because queue and webhook documentation assumes it.
| Guarantee | Sender behaviour | Risk |
|---|---|---|
| At-most-once | Send, never retry | Messages can be lost |
| At-least-once | Retry until acknowledged | Messages can arrive more than once |
| Exactly-once | — | Not achievable as a delivery guarantee |
The third row is the interesting one. Exactly-once delivery cannot be built, for the reason above: the sender can never distinguish a lost message from a lost acknowledgement, so it must either resend or not, and each choice forfeits one of the guarantees. No protocol removes that choice.
What is achievable, and what everyone actually wants, is exactly-once effect. You get it by combining at-least-once delivery with idempotent processing: let the sender retry freely, and make the receiver's handling of a duplicate a no-op. Systems advertising “exactly-once semantics” are, without exception, describing that combination.
Which is why webhook providers deliver with at-least-once semantics and tell you to deduplicate. Your receiver will occasionally see the same event twice, most often when your acknowledgement was slow rather than because anything went wrong. Store the provider's event identifier, ignore identifiers you have already processed, and acknowledge quickly — a receiver that does heavy work before responding invites the timeout that causes the redelivery. The webhook tester covers inspecting those payloads.
Idempotency keys
Some operations cannot be made naturally idempotent. “Charge this card £40” is a genuine accumulation:
there is no URL to PUT to and no way to express the request such that repeating it is
inherently harmless. For these, the standard answer is an idempotency key — the pattern
payment APIs converged on, and the reason a POST to a modern payments endpoint is safe to
retry.
POST /v1/charges HTTP/1.1
Content-Type: application/json
Idempotency-Key: 018f3c7a-6b21-7e44-9d2e-5c1a0f8b7e33
{ "amount": 4000, "currency": "gbp", "source": "card_1M2n…" }
The client generates a unique value — a UUID is the usual choice — and the server records it with the result of the operation. A second request bearing a key the server has already completed does not execute anything; the stored response is returned instead. The client sees what looks like a successful call and has no duplicate charge.
The critical rule is on the client side. The key identifies the intention, not the attempt. Generate it once, when the user presses the button, and reuse the identical value on every retry of that operation — including retries that happen minutes later, after a process restart, or from a job queue. A client that generates a fresh key inside its retry loop has implemented a slower way of creating duplicates.
Implementing the server side
The details are where correctness lives, and most of them are not obvious.
- Scope the key. Store it per account and per endpoint, not globally. Two customers generating the same UUID is unlikely, but a key stored globally lets one tenant's key collide with another's, which is both a bug and an information leak.
- Fingerprint the request. Record a hash of the body alongside the key. If the same key arrives with a different payload, that is a client bug — reject it explicitly rather than replaying an unrelated stored response. 409 Conflict is the appropriate status, and the body should say plainly that the key was reused with different parameters.
- Handle the in-flight case. A retry frequently arrives while the original request is still executing — that is precisely what a timeout implies. Record the key before doing the work, in the same transaction if you can, and answer a second request that finds the key present but unfinished with a 409 telling the client to retry shortly. Without this, concurrent duplicates slip past the check entirely, which is the single most common way a key implementation fails in production.
- Store the whole response. Status code, body and any headers a client needs. The point is that a retry is indistinguishable from the original, so an identifier minted on the first call must come back on the second.
- Expire keys. Twenty-four hours is a common window. Retention is not free, and a key older than any plausible retry serves no purpose. Document the window, because a client retrying a day-old operation needs to know it will execute again.
- Do not key failures the client should fix. A 422 from invalid input is not an outcome worth replaying; let a corrected request through. Store outcomes that had side effects.
The header name is a convention rather than an internet standard, and spellings vary between providers, so check the documentation of the API you are calling. It is being formalised, but for now treat it as a widespread pattern rather than something a generic client will do for you.
Making your own endpoints idempotent
Keys are not the only tool, and often not the first one to reach for.
Let the client choose the identifier
If a client can name the resource it is creating, creation becomes a PUT and idempotency
comes free with the method — no key store, no expiry policy, no in-flight handling. This is the cleanest
answer whenever the thing being created is a document rather than an event, and
POST vs PUT covers the trade-offs of moving identifier choice to the
client.
Use a natural uniqueness constraint
When the payload already contains something that must not repeat — an invoice number, a customer's own order reference, an event identifier from an upstream system — enforce uniqueness in the database and answer a second attempt with 409. It costs nothing extra and works for clients that have never heard of idempotency headers.
Model the operation as a state transition
“Set status to shipped” is idempotent; “advance to the next status” is not. Where you can,
express writes as assignments of a target state rather than as increments or advances, and a replay
becomes a no-op by construction. The same reasoning applies to counters: storing a set of contributing
event identifiers and deriving the count is idempotent, whereas count = count + 1 never can
be.
Guard with a precondition
Conditional requests make a write safe in a different way. If-Match with an entity tag applies
the change only if nothing else has written since you read, and If-None-Match: * applies it
only if nothing exists yet. Both turn a replayed request into a visible
412 rather than a silent second effect.
Retry policy in practice
Knowing an operation is idempotent tells you that retrying is permitted. It does not tell you how to retry, and a badly built retry loop converts a brief blip into an outage.
| Response | Retry? | How |
|---|---|---|
| Connection failure or timeout | Yes, if idempotent or keyed | Exponential backoff with jitter |
| 429 Too Many Requests | Yes | Honour Retry-After; do not back off faster than it says |
| 503 Service Unavailable | Yes | Backoff; check for Retry-After |
| 504 Gateway Timeout | Cautiously | The upstream may have completed the work — treat as unknown outcome |
| 500 Internal Server Error | Cautiously | Also an unknown outcome; retry only with idempotency |
| 400, 422 | No | The request is wrong; retrying it unchanged wastes both sides' time |
| 401, 403 | No | Refresh the credential first, then send a new request |
| 409 Conflict | Only after re-reading | Rebase your change on current state; a blind retry conflicts again |
Three rules make the difference between a retry policy that helps and one that hurts. Add
jitter: without randomisation, every client that failed during an outage retries at the same
instant and knocks the service over again the moment it recovers. Cap the attempts, and
prefer a retry budget — a limit on the proportion of total traffic that may be retries — over per-request
counts, since unbounded per-request retries multiply load exactly when the system can least afford it.
And respect Retry-After, which is the server telling you what it can
actually take; API rate limiting covers the headers that carry
that information.
Testing it
The property is easy to assert and easy to get wrong, so verify it directly rather than reasoning about it. Three checks catch most defects.
Send the request twice and inspect the state. Not the responses — the stored data. From
the API tester, send the write, then send it again, then GET the collection
and count. This is a thirty-second test that finds the majority of real problems, and it is the one people
skip because the endpoint is “obviously” idempotent.
Send the same idempotency key concurrently. Sequential retries pass trivially; the interesting case is two requests overlapping, which is what actually happens when a client times out and retries while the first request is still running. If both execute, your key check runs too late in the request lifecycle.
Send the same key with a different body. The server should refuse with a 409, not silently return the earlier result. Replaying an unrelated stored response for a genuinely different request is worse than duplicating, because it looks like success.
From here, the HTTP methods guide covers safety and cacheability alongside idempotency, POST vs PUT covers the creation case in detail, REST API testing covers turning these checks into a repeatable suite, and the status code reference explains every code named above.
Frequently asked questions
What does idempotent mean in an API?
Performing the operation many times leaves the server in the same state as performing it once. The definition is entirely about effect on stored state and says nothing about responses, which may legitimately differ between the first call and later ones.
Is DELETE idempotent if the second call returns 404?
Yes — this is the example that proves the definition concerns state, not responses. The first call
removes the resource and returns 204; the second finds nothing and returns
404. The state after one call and after five is identical: the
resource does not exist.
How does an idempotency key work?
The client generates a unique value per logical operation — usually a
UUID — and sends it in an Idempotency-Key header. The
server stores the key with the outcome and, on seeing it again, returns the stored response without
executing anything. Generate the key once per intention and reuse it on every retry.
Is it safe to retry a request that timed out?
Only if the operation is idempotent, or has been made so with a key. A timeout tells you nothing about whether the server acted — the failure could have been before, during or after processing. With idempotency the ambiguity stops mattering; without it, a blind retry may duplicate a record or take a second payment.
What is the difference between safe and idempotent?
Safe means the request is not intended to change state at all, as with GET and
HEAD. Idempotent means repeating it adds nothing beyond the first application. Every safe
method is idempotent; the reverse does not hold, since PUT and DELETE change
state and are idempotent anyway.
Why can't a system guarantee exactly-once delivery?
Because a sender cannot distinguish a lost request from a lost acknowledgement, so it must either resend and risk duplicates or not resend and risk loss. Real systems choose at-least-once delivery plus idempotent processing, which produces exactly-once effects — the thing people actually want.