Idempotency Key
An idempotency key is an identifier a client attaches to a request so that the server can recognize a repeat of it as the same operation rather than a new one. It exists to solve one specific problem: a client that sent a request and never received a response cannot tell whether the server acted.
RFC 9110 defines idempotency through the intended effect of repeating an identical request. PUT, DELETE, and safe HTTP methods are idempotent under those semantics. A POST that creates an object or charges a payment often needs an additional mechanism to prevent a retry from creating another effect.
For a non-idempotent method, RFC 9110 advises against automatic retries unless the client knows the operation is effectively idempotent or can detect that the original was never applied. A correctly implemented key mechanism can supply that operation-level guarantee within its documented scope and lifetime.
Distinguish this from the property itself: idempotency is what you want, and a key is one way to get it where the operation does not have it naturally.
What the server has to do
Accepting a key is easy; honoring it correctly is five decisions.
- Reconnect repeats to the same operation and outcome. The contract may replay the original response, return the operationโs current status, or provide a result location. PayPal, for example, documents returning the current status rather than the original status. Store durable evidence that connects the key to the effect. For local database effects, committing that evidence with the change closes the crash window; storing the entire HTTP response is one design, not a universal requirement.
- Compare the request, not only the key. The same key arriving with a different body is a client bug, and the useful behavior is to reject it rather than silently return the earlier result โ otherwise a key collision quietly discards an operation. A hash of the meaningful request content, stored alongside the key, is enough.
- Scope the key. Per caller at minimum, usually per caller and operation. A global key space means one client’s identifier can collide with another’s.
- Define the retention period. After the key record expires, another request may execute again. That does not guarantee a duplicate, but the key no longer guarantees its prevention. Choose and document a window that covers supported retry and recovery schedules, and require callers to stop automatic retries before it ends.
- Handle the concurrent case. Two requests with the same key can arrive at once, which is exactly what an aggressive retry produces. Something has to serialize them โ a unique constraint on the key, or a lock โ and the second one should wait for or reflect the first rather than proceeding in parallel.
Four outcomes, not two
The most consequential design question is what a stored record says when the operation did not cleanly succeed โ and the tempting answer is dangerous. Cache permanent failures, do not cache transient ones so the retry can succeed sounds right and can charge a customer twice, because an HTTP error is not evidence that nothing happened. A payment can be captured and the response then fail with a 500.
So the record needs four states rather than two.
| State | What is known | What a repeat should do |
|---|---|---|
| Complete | The effect happened and durable evidence identifies the operation and outcome | Return the recorded response, current status, or result location as the contract specifies |
| In progress | Another attempt holds the key right now | Wait, or answer in progress โ never start a second attempt |
| Confirmed not applied | Evidence establishes that no effect occurred and no earlier attempt can still apply it | Keep rejecting invalid input. Retry a valid request only when the failure and operation contract permit it |
| Indeterminate | An attempt was made but its effect is unknown after an error or lost response | Do not start an unprotected new effect. Use status lookup, reconciliation, or a dependencyโs documented retry with the same operation key |
A permanent rejection before any effect, such as invalid input, can be cached to avoid repeated work. The client fixes the request and resends it under whatever key and parameter rules the API specifies.
The opposite mistake is treating every error as that state. Stripe illustrates why an error cannot be treated as proof of non-application. It caches 500 responses for POST mutations that reached its idempotency layer. Those responses can remain unchanged even while Stripe investigates or reconciles effects. A fresh key can therefore duplicate an action whose first response reported an error.
After a network failure, retrying with the same key and parameters can recover a response within the providerโs retention window. Receiving a response does not necessarily settle the business outcome: a cached 500 can still mean the effect is unknown. Bound attempts and elapsed time, respect the APIโs retry guidance, and stop before the keyโs protection expires.
Unresolved operations need a recovery path. Query the dependency by operation identifier, process its callback, or use periodic reconciliation. When retries keep returning an indeterminate result, switch to these paths and escalate cases that remain unresolved. Preserve that status and the original identifiers while the case is open. Do not substitute a new key merely to get past the uncertainty.
Define which business effects the key protects. A unique local order row does not by itself protect a separate payment call. External effects need a protocol that connects durable local intent to the dependencyโs operation identity and recovery behavior, including duplicate protection where required. Recording an intention locally is not an atomic commit with an external processor.
RFC 9110 limits idempotency to the intended effect requested by the user; repeated logs or revision records are allowed. Apply duplicate protection to the promised business effects, such as one charge or one requested notification. Whether other downstream calls must be deduplicated depends on their semantics, not merely on their being calls.
What the client has to do
Two rules, and the first is the one that gets broken.
Generate the key before the first attempt, and reuse it for every retry of that operation. A key generated per attempt provides nothing โ each retry looks like a new operation, which is the situation you were trying to avoid. That means the key belongs to the business action, so it usually has to be created where the action is initiated and carried through whatever retry machinery sits below.
Never reuse a key for a different operation. Persist the key before sending so that it survives a restart. A stored random UUID and a stable request-record identifier can both work. An order ID alone may be too broad when one order has separate authorization, capture, and partial-refund operations; give each action its own identity within the APIโs key scope.
The surrounding retry behavior still needs care, and RFC 9110 has two lines worth heeding: “a proxy MUST NOT automatically retry non-idempotent requests,” and “a client SHOULD NOT automatically retry a failed automatic retry.” Spacing the attempts is the other half โ see Retry-After and backoff โ since a key makes repeats safe for correctness, not free for the server.
Four ways it is implemented wrongly
- The key is recorded but the outcome is inaccessible. A bare duplicate error leaves the caller unable to tell whether the original succeeded. Return the operationโs outcome or a documented way to retrieve it; the first HTTP response need not be reproduced byte for byte.
- The effect commits before its duplicate-protection evidence. A crash between those steps can allow the effect to run again. Commit local data and its guard together, and use the dependencyโs recovery protocol for external effects.
- The key is optional and rarely sent. A mechanism only clients who read the documentation use protects only them. Where the operation charges money or creates something visible, requiring the key is the safer contract.
- The expiry is undocumented. A retry an hour later against a ten-minute key window can run as a new operation. Callers need the retention boundary and a recovery path for unresolved work after automatic retries stop.
A naturally idempotent operation may not need a separate retry key for its intended resource effect. A PUT can repeatedly set a known resource, and a business-key constraint can prevent a duplicate row. These mechanisms do not automatically protect external charges or prevent a delayed write from replacing newer state. Use conditional writes or version checks when concurrent updates must be preserved, and protect additional business effects separately. How these guarantees fit the wider interface contract is worked through in You Cannot Un-publish an API.
References
- RFC 9110, HTTP Semantics, Idempotent Methods
- Stripe Documentation, Advanced error handling
- PayPal, Idempotency
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.

Clear explanation, especially the distinction between idempotency as the property and the key as one mechanism to get it. The part teams most often get wrong in my experience is what the server returns on the replay: same status and body as the original, or a conflict while the first request is still in flight. Firing the identical POST twice by hand with the same key, for example from https://tryapi.dev/, is a quick way to check an implementation actually behaves the way its docs claim.