Idempotency Keys for Reliable HTTP APIs: Semantics, Storage, and Failure Recovery
A rigorous guide to designing idempotency keys for HTTP APIs, including request fingerprints, replay semantics, storage guarantees, TTL, retries, multi-region trade-offs, and recovery from partial failures.
Learning outcome
By the end of this lesson, you should be able to design and evaluate an idempotency-key strategy for a production HTTP API that can survive retries, timeouts, lost acknowledgements, and partial failures without creating duplicate business effects. You should also be able to explain when to return the original response, when to reject a replay, how long to retain keys, and what consistency level is needed in storage.
Intuition
Idempotency keys exist because the network is not reliable even when your application code is. A client may send a request, the server may process it, and the response may never arrive due to timeout, connection reset, proxy failure, or a crashed intermediary. The client then retries. Without protection, a retry can create a second charge, second order, second shipment, or second email.
The key idea is simple: the client supplies a unique token for a logical operation, and the server treats repeated requests with the same token as the same operation. The server does not merely ignore duplicates; it must decide what “same” means. In practice, that means binding the key to a request fingerprint, the target tenant or account, and often the endpoint or semantic operation.
A useful mental model is: the idempotency key names an intent, not just a packet. If the first attempt succeeded, the retry should replay the same result. If the first attempt is still in flight, the retry should usually wait, return an accepted/in-progress status, or be rejected with a conflict depending on the API contract. If the retry carries the same key but a materially different request body, it should not silently reuse the prior outcome.
Deep dive
A robust design usually has four parts:
- Key generation and scope
- The client generates a high-entropy key per logical operation.
- Scope the key to at least the authenticated principal or tenant, and often to a specific API route.
- Avoid global reuse across endpoints unless the semantics are intentionally shared.
- Request fingerprint
- Store a canonical fingerprint of the request payload and relevant headers.
- The fingerprint should include fields that change business meaning: amount, currency, destination account, item list, shipping method, and similar attributes.
- Exclude volatile transport fields like timestamps added by gateways, trace IDs, or dynamically reordered JSON if you canonicalize before hashing.
- The fingerprint helps distinguish a true retry from a client bug that accidentally reuses a key for a different operation.
- Atomic reservation and response replay
- On first observation, reserve the key before executing the side effect.
- If another request arrives with the same key while the first is pending, the server must avoid a race where both proceed.
- After success, persist enough response metadata to replay the same status code, headers where appropriate, and body.
- If the first attempt fails before the business effect is committed, the server must know whether the operation is safe to retry or whether it has become ambiguous.
- Retention and expiry
- Idempotency data cannot live forever.
- The retention window should cover realistic retry behavior and client backoff windows, often hours to days for payment-like APIs, shorter for non-critical workflows.
- After TTL expiry, a reused key may be treated as new, which is acceptable only if the business domain tolerates it and clients are documented accordingly.
A good store record often includes: idempotency key, tenant/account, route or operation name, request fingerprint, status of processing, response code, response body or a pointer to it, created time, expiry time, and optionally a hash of important headers.
Decision table
| Incoming request state | Matching key? | Matching fingerprint? | Typical response |
|---|---|---|---|
| First use | No | N/A | Atomically claim key, then execute the protected operation |
| Retry after success | Yes | Yes | Replay original response |
| Retry while pending | Yes | Yes | Conflict, wait, or accepted/in-progress |
| Same key, different payload | Yes | No | Reject as misuse, often 409 or 422 |
| Key expired | Maybe | Maybe | Treat as new or reject if policy forbids |
| Store unavailable | N/A | N/A | Prefer fail closed for side-effecting operations |
Storage and database constraints
Idempotency only works if the reservation is atomic. Typical approaches include:
- A unique constraint on
(tenant_id, idempotency_key)or(tenant_id, route, idempotency_key). - An insert-first pattern that succeeds only once, causing concurrent duplicates to lose the race.
- A transaction that writes the reservation row and business record together when possible.
- A state machine that distinguishes
pending,succeeded,failed-before-effect, andoutcome-unknown; retention expiry is a separate policy decision. A genericfailedflag is insufficient to decide whether execution is safe.
An atomic claim is necessary for this reservation design, but it is not sufficient to make the business effect atomic with the claim. It is not enough to “check then insert” because two concurrent requests can both pass the check. The database or strongly consistent store must arbitrate the race.
Response replay semantics
Replay does not always mean “return the exact same bytes.” It means “preserve the same observable business outcome.” For example:
- Same 201 Created with the same resource identifier for a successful create.
- Same 200 OK and business payload for a read-modify-write operation that returned a receipt.
- Same error if the first attempt deterministically failed before side effects.
Be careful with headers: some are safe to replay, others are request- or time-specific. You may need to replay only the business-relevant parts and regenerate trace headers or server timing.
Failure recovery
The most dangerous failure mode is the ambiguous commit: the server may have completed the business action but crashed before writing the idempotency record, or vice versa. Recovery depends on the domain:
- For payments, you may need a durable business transaction identifier and reconciliation job.
- For orders, you may need a status table that the retry can query.
- For emails or webhooks, you may need deduplication downstream as well.
When the outcome is unknown, keep it explicitly unresolved and offer the documented status/reconciliation path. Repeating the same key must not automatically repeat the side effect. Do not delete the record, mark it safe to retry, or recommend a new key merely because a worker lease or HTTP deadline expired.
Multi-region trade-offs
Multi-region deployments introduce a hard trade-off between latency and duplicate prevention.
- Strong global coordination reduces duplicates but adds latency and operational complexity.
- Regional keys with local stores improve latency but allow cross-region duplicates if clients fail over and retry elsewhere.
- Asynchronous replication can create race windows where the same key is accepted in two regions before replication converges.
A common interview answer is that payments and other irreversible actions often need either a single-writer region, a globally consistent store, or an external ledger with stronger guarantees. Lower-stakes operations may accept eventual consistency with compensating cleanup.
Observability
You should log and metric idempotency behavior explicitly:
- key accepted
- replayed response
- payload mismatch
- key expired
- pending duplicate
- reservation conflict
- storage failure
These signals reveal client retry storms, bad SDK usage, and backend race conditions. Use controlled correlation fields in access-restricted logs. Prefer a non-sensitive key digest where raw keys are unnecessary; keep keys and business IDs out of metric labels to avoid an unbounded label set. Do not log sensitive payloads blindly.
Failure modes
Common misuse warnings:
- Reusing keys across different operations: creates accidental suppression or false replay.
- Non-canonical fingerprints: semantically identical JSON with different formatting can look different if not normalized.
- Storing only the key and not the fingerprint: allows accidental misuse to replay the wrong operation.
- Too-short TTLs: retries after network partitions become duplicates.
- Long retention without a policy: storage growth, privacy obligations, and stale response replay. Decide which response data may expire while a durable business identity still prevents duplicate creation.
- Check-then-act races: both requests observe absence and both process.
- Treating 500 as always retryable without examining side effects: can duplicate partially committed work.
- Assuming one retry library across all clients: mobile, browser, and server-to-server traffic have different retry patterns.
Micro-tests
Use these as interview sanity checks:
- If two identical create-payment requests arrive simultaneously with the same key, can only one payment be charged?
- If the second request changes the amount but reuses the same key, does the server detect the mismatch?
- If the first request times out after committing, does the retry receive the original receipt or create a second one?
- If the idempotency store is temporarily unavailable, does the API fail closed or risk duplication?
- If a key expires after 24 hours, what happens to a legitimate late retry?
Concrete HTTP scenarios
- POST /charges: The client times out, retries with the same key, and must get the same charge ID back.
- POST /orders: The server creates the order row but crashes before responding. The retry must not create a second order.
- POST /webhooks/send: Use a durable event identity and deduplicate the consumer’s business effect. A lost acknowledgement can still cause repeated HTTP deliveries. An outbox can make local event creation durable with a local business commit; it does not make remote delivery exactly once or atomically commit a remote HTTP effect.
- PATCH /profile: Compare “set display name to Ada” with “increment revision and send a notification.” Define the intended business effects before choosing protection. An audit entry per attempt is a separate diagnostic policy; it does not alone establish whether the profile operation is idempotent.
Worked recovery trace: one order, two stores
Assume an order API claims (tenant-A, create-order, key-K) in store S, then writes order 731 in database B. A unique claim in S has only proved that one worker won the claim; it has not made the write in B atomic with S.
- Worker A claims K. Worker B receives the same request and sees
pending; B does not create an order. - A commits order 731 in B. A crashes before S stores the receipt.
- The client retries K. S still says
pending, although the order exists. - A recovery worker queries the durable business operation identity linked to K. If it finds order 731, it records/replays that outcome. If it cannot establish the outcome, it keeps the operation unresolved rather than blindly issuing a second create.
If the order and key-result record live in the same transactional database, commit both with the same transaction and enforce the operation identity there. That removes this particular split-commit window, not every downstream failure. If charging, shipment, or email occurs over HTTP, design that boundary independently with a stable downstream identity and reconciliation. A database transaction cannot roll back a remote service’s already completed request.
Lease-expiry counterexample: A pauses after sending the external request. Its lease expires; B takes over and sends again; A’s first request then completes. Claiming a new lease did not prove the first effect was absent. Recovery needs a sink that enforces the same operation identity, appropriate fencing where supported, or reconciliation before re-execution.
Worked answers to the micro-tests
- Simultaneous requests: One atomic claim wins, and losers follow the pending/replay branch. To promise one charge, the payment boundary must also enforce the operation identity; the claim alone is insufficient.
- Amount changed: Compare the stored, versioned request fingerprint before returning a receipt. For example, K with
amount=500cannot replay a receipt for K withamount=700. Choose and document the mismatch status; 409 and 422 in the table are policy examples, not a universal requirement. - Timeout after commit: Use the same key and recover the original business identity/receipt. A client timeout says nothing conclusive about whether the server committed.
- Store unavailable for 30 seconds: In a fail-closed contract, return an availability failure before beginning an unprotected effect. Do not fall back to a per-process dictionary across replicas. Existing in-flight operations require reconciliation if their outcome is uncertain.
- Late retry: For an example policy retaining a result for 48 hours, a retry at hour 47 can replay; a retry after the record is removed cannot rely on that record. Keep an independent unique business operation ID or reject stale requests if duplicates are unacceptable. A 24-hour or 48-hour window is not a universal idempotency guarantee.
Concrete schema exercise answer
For this order example, store a unique (tenant, operation, key), fingerprint version and digest, processing state, durable order/operation ID, creation/retention times, and the contract’s replay status/body reference. Claim ownership and transitions must be atomic. Authenticate and authorize the caller before either execution or replay; knowing K must not reveal another tenant’s receipt. Define separately which validation failures are recorded, whether terminal failures are replayed, and how callers check unresolved work.
Interview drill
Try answering these in under two minutes each:
- What exactly is an idempotency key protecting: transport retries, business logic, or both?
- Why is a unique database constraint better than an application-level existence check?
- What do you return if the same key is reused with a different payload?
- How do you design for a request that may have succeeded but whose response was lost?
- What are the multi-region failure windows for duplicate creation?
- How would you size the retention TTL for a payments API versus a comment-posting API?
Exercise prompts
- Design the schema for an idempotency table that supports replay and mismatch detection.
- Explain how you would prevent duplicate charges during a retry storm.
- Describe a fallback strategy if the idempotency store is unavailable for 30 seconds.
- Compare synchronous global coordination with regional local writes for a high-volume checkout API.
Revision checklist
Before an interview or design review, make sure you can explain:
- the difference between safe retries and duplicate side effects
- request fingerprinting and canonicalization
- atomic reservation with unique constraints or equivalent strong guarantees
- response replay versus new execution
- TTL, retention, and expiration policy
- ambiguous commit recovery
- observability metrics and logs
- multi-region consistency trade-offs
- how to reject key reuse with mismatched payloads
If you can walk through a charge API, an order API, and a webhook delivery API with these rules, you understand idempotency keys at production depth.
Reference
- Stripe idempotent requests (a specific provider contract; consult it for Stripe integrations rather than assuming its policy applies to every API)