When a client loses the response to an order submission, the server needs a way to recognize a later attempt as the same purchase. Sending the order again with a new identity could create another purchase. Sending it with the same identity gives the server a way to recognize the original intention and return its result.
An idempotency key supplies that identity. Making it reliable requires more than adding a header: the API must define what the key identifies, how the server compares requests, which outcomes it retains, and what happens when processing stops unexpectedly.
An idempotency key is a client-supplied identifier that a supporting server uses to associate multiple request attempts with one logical operation. The server prevents those attempts from independently repeating the operation's intended effects within its documented guarantee.
Generate the key before the first submission and retain it with the operation's request data. Reuse both for attempts to complete that same operation. A deliberately new purchase gets a new key, even if its books and delivery address are identical.
The client should persist this association wherever its workflow can resume. A browser checkout might need to survive a reload; a background job needs to survive a worker restart. Generating a fresh key inside a function that runs for every HTTP attempt defeats duplicate recognition.
Use a well-established random identifier generator, such as a UUID version 4 generator. A timestamp alone can collide, and a customer ID identifies a customer rather than one purchase. Hashing only the order body also confuses legitimate identical purchases with duplicates.
A request or trace ID serves a different purpose: identifying processing for observability. Different attempts can have different trace IDs while sharing one idempotency key. The key is also not the resulting order ID; the client needs it before the server creates the order.
The relationship looks like this when the first response never reaches the client:
The second attempt recovers the first operation's result. It does not create another order and then try to undo it.
The examples use a fictional bookstore API over HTTPS at api.bookstore.example. Its POST /orders endpoint requires an Idempotency-Key header containing one quoted UUID string. This is the bookstore's chosen contract. Do not assume that every API accepts this header, uses the same syntax, or implements the same retention and error policies.
An order here contains requested books and a delivery address. Creation records an order with status pending_payment; it does not charge a payment method, reserve inventory, or send notifications. Keeping these effects outside the example's transaction makes its guarantee precise.
The bookstore defines the following behavior:
A request fingerprint is a stable representation or hash of the input. The service uses it to decide whether two requests describe the same operation and must define exactly which input it includes.
The status choices and 48-hour window above are API policies. HTTP status definitions do not establish a universal idempotency-key contract.
A customer submits:
The service creates the order and retains this result:
All credentials are placeholders. HTTP JSON bodies are exactly the single lines these examples show, without trailing newlines.
Repeating the exact request while the server retains its completed result returns 201, the same JSON body, and the same Location. It does not mean another creation occurred. Transport and operational metadata, such as Date or a request-tracing header, can differ.
This is a replay of the creation outcome, not a current order lookup. If the customer has since paid for or cancelled the order, the stored body still describes its original creation result. An authorized GET of the order provides current state.
Cache-Control: no-store prevents ordinary HTTP caches from storing the response. It does not prohibit the application from maintaining the durable operation record that implements this contract.
The bookstore looks up a key within this namespace:
It derives the account from verified authentication, rather than trusting an account ID the client supplies in the JSON body. Two accounts using the same key do not collide or receive each other's responses. The same key on a different endpoint is a different lookup under this policy; other APIs may instead require account-wide uniqueness.
Refreshing an access token for the same account should not change the namespace. Scoping by the literal token would make ordinary credential refresh look like a new operation. Whether multiple users within an organization share a namespace is a separate design decision that must agree with the authorization model.
Within the namespace, the fingerprint binds the key to the original intent. This endpoint includes its supported content type and normalized request body. It rejects query parameters rather than silently ignoring them. An API that uses query parameters or version-selection headers to change behavior must account for those inputs too.
The bookstore's normalization ignores JSON whitespace and object-member ordering. It preserves array order, distinguishes omitted fields from explicit nulls, and validates quantities as integers. The server rejects duplicate JSON member names. Equivalent formatting therefore matches, while changing quantity from one to two does not.
This normalization is a service choice. Comparing exact body bytes is simpler but would reject requests that differ only in formatting. Either approach can work if clients know the rule. Do not normalize away meaningful distinctions, such as the order of requested actions in an array.
If you store fingerprints as hashes, use a suitable cryptographic hash and a stable normalization algorithm. Retain its version if the algorithm can change, so a deployment does not reject valid repetitions of older requests. A hash of sensitive request data is not automatically safe to expose or retain indefinitely.
Authorization tokens and tracing headers do not enter this example's fingerprint. Their values may change across attempts without changing the purchase intention. The server still checks authorization on every attempt, including before returning a stored result.
A key binds to accepted input; it is not permission to change a request until it succeeds. Consider a second request using the retained key but changing the quantity:
The bookstore rejects the mismatch without creating or changing an order:
This error does not replace the original stored result. A new key is appropriate for a genuinely new operation, but changing keys is not a recovery technique for an uncertain purchase. First establish what happened to the original operation.
A matching request can also arrive while the original is executing. This API returns:
The conflict describes this attempt, not the final outcome of the operation. The server does not store it as the operation's terminal result. Other contracts may wait briefly for completion or return a documented operation resource; choose one behavior deliberately.
Validation and permission failures need equally clear treatment. This API rejects requests with malformed JSON or schema errors before reserving the idempotency key for the request. For example, quantity zero receives 422 with validation_failed, and the server creates no order. If no request has already claimed that key, correcting this rejected input can use it because no binding exists yet. That allowance applies to this documented pre-execution failure, not to every 422 response.
A validly authenticated account that no longer has permission to create orders receives 403 with order_creation_forbidden, even when its key has a completed result. The server replays no stored order data. The original record remains intact. Missing or invalid credentials receive the API's ordinary authentication error before key lookup.
The point at which execution starts also affects business failures. Here, the service can reject a well-formed, authorized request during execution because a requested book is no longer orderable. The service treats that 422 business rejection as a completed result and retains it. A matching duplicate receives the same rejection even if catalog availability changes later.
Do not decide what to retain solely from the status-code class. A 500 can occur before any work commits, after work commits, or while its outcome is uncertain. Those situations need different internal treatment even if the client initially sees the same status.
The server needs durable storage that every instance processing the operation shares. A process-local map loses its contents on restart and cannot coordinate two servers.
An operation record can contain:
The uniqueness constraint on scope and key prevents two records for the same operation. It does not, by itself, make the order write and result write atomic.
The following implementation assumes the operation records and orders are in the same transactional database. All workers follow the same locking protocol, and the transaction contains no external service calls:
pending record with its fingerprint under a database uniqueness constraint. If it already exists, use the existing record and check the fingerprint.Commit the key claim before starting the business transaction so another worker can find the pending operation after a failure. Then commit the business change and its completed result together:
If the worker crashes before this transaction commits, the database rolls back its order and result changes. The pending claim remains available for recovery. If it crashes after commit, another worker finds the completed result. There is no committed order without its completed operation record under these assumptions.
An intentionally flawed alternative writes the order first and stores the result afterward in an independent transaction. A crash between those writes leaves the next worker unable to distinguish “server created no order” from “server created order; result missing.” Make the two writes succeed or fail together; do not rely on them usually happening close together.
A fast cache can help with lookups, but eviction must not remove the only evidence that an order already exists. If the authoritative operation store is unavailable, this endpoint rejects or defers new work rather than creating an order without duplicate protection.
A pending record means the server has recorded no terminal result. It does not necessarily mean a worker is still alive. Recovery must determine whether it can safely continue the operation.
For the local database design, acquiring the record lock establishes that no conforming worker currently holds that lock. The recovering worker then rechecks completion and runs the same transaction if necessary. A timer alone is not sufficient evidence: a slow worker might still be active.
The lifecycle has few states, but each transition must preserve the idempotency guarantee:
Pending records do not become new operations merely because they are old. Completed records replay their retained result, and cleanup occurs only after the promised retention window.
External effects require more than this local transaction. If order creation also calls a payment service, rolling back the bookstore database cannot undo a payment that service already committed. A payment timeout must leave the operation unresolved until the bookstore can safely establish the outcome.
Give each distinct effect a stable operation ID that the downstream service supports for duplicate prevention or reliable outcome lookup. An order's payment and refund should not accidentally share an identity that means the same operation to that service. Changing the downstream key on every recovery attempt recreates the duplicate problem.
For asynchronous work, store the order and a durable work item in one transaction so a crash cannot erase the record of work to send. The worker still needs duplicate protection when delivering that work to another system. The existence of a top-level key does not automatically extend its guarantee across every dependency.
The bookstore retains completed results for at least 48 hours from completion. Replaying a result does not extend that period. A delayed cleanup job may retain records longer, but clients cannot depend on extra time beyond the guarantee.
After removal, the same key is indistinguishable from an unused key in this design. Reusing it can create another order. The client should stop relying on replay before the window expires and recover an uncertain outcome through the service's lookup or support process. A newly generated key provides no evidence that the earlier purchase failed.
Choose retention according to actual client behavior, including offline recovery and delayed jobs. Storage needs grow with operation volume, response size, and retention duration. If business rules require permanent uniqueness, enforce that in durable business data rather than depending on a short-lived response record.
Do not expire unresolved pending work using the completed-result cleanup policy. Investigate old pending records and reconcile them under the execution protocol. Otherwise, cleanup can turn an unfinished operation into an apparently new one.
Keys are untrusted input and are not credentials. Bound their length, validate their format, avoid embedding personal data, and apply authorization before exposing stored results. Protect retained response bodies as customer data. The API still needs rate limits and storage quotas because an abusive caller can generate unlimited new keys.
Regions that accept keys in the same namespace must coordinate so the same key cannot create separate orders in different regions. Independent regional stores that both accept an unseen key can both create an order. Route the operation to one authoritative owner or use storage that enforces the required uniqueness and transaction behavior across those requests.
Operationally, track replay frequency, fingerprint mismatches, pending age, and failures to access the operation store. Unexpected mismatches often reveal clients changing input under an existing key; excessive new keys after timeouts can reveal clients discarding operation identity. These observations help confirm that the published contract survives real client behavior.
Generate one key for each logical operation and preserve it with the request across attempts. Define its namespace, input-matching rules, replay behavior, error handling, and retention window explicitly.
On the server, coordinate duplicate claims and commit business changes with their recoverable results. Handle unresolved external effects before attempting them again, and keep authorization active on replay. The key identifies the operation; durable storage and a correct execution protocol make the guarantee real.