Idempotency

An idempotent API operation is one you can perform repeatedly without doing any more damage than performing it once. That sounds like a minor technical property until you notice what depends on it: every retry policy, every message queue, every payment integration and every webhook receiver. This guide gives the definition precisely — including the part almost every summary gets wrong — then covers idempotency keys, delivery semantics and when a retry is actually allowed. Experiment as you read with the API tester.

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.

MethodSafeIdempotentWhy
GETYesYesA read; changes nothing to repeat
HEADYesYesA read with the body suppressed
OPTIONSYesYesAsks what is permitted
TRACEYesYesEchoes the request back
PUTNoYesAssigns a complete state; assigning it twice is assigning it once
DELETENoYesConverges on absence after the first call
POSTNoNoAccumulates — each call adds another member or event
PATCHNoNoA 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.

GuaranteeSender behaviourRisk
At-most-onceSend, never retryMessages can be lost
At-least-onceRetry until acknowledgedMessages can arrive more than once
Exactly-onceNot 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.

ResponseRetry?How
Connection failure or timeoutYes, if idempotent or keyedExponential backoff with jitter
429 Too Many RequestsYesHonour Retry-After; do not back off faster than it says
503 Service UnavailableYesBackoff; check for Retry-After
504 Gateway TimeoutCautiouslyThe upstream may have completed the work — treat as unknown outcome
500 Internal Server ErrorCautiouslyAlso an unknown outcome; retry only with idempotency
400, 422NoThe request is wrong; retrying it unchanged wastes both sides' time
401, 403NoRefresh the credential first, then send a new request
409 ConflictOnly after re-readingRebase 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.