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 | Process and reserve key |
| 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 with
pending,succeeded,failed, andexpiredstates.
The unique constraint is often the hardest requirement to satisfy correctly in distributed systems. 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, the API should prefer a response that tells the client to retry safely or query status rather than inventing a false success.
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. Trace the idempotency key, tenant, route, and business identifier. 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.
- Too-long TTLs: storage growth, stale replays, and key exhaustion risks.
- 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: A sender retried by its own queue needs deduplication so downstream consumers do not receive multiple deliveries.
- PATCH /profile: Idempotency keys may be unnecessary if the patch is naturally idempotent, but they can still help when the operation triggers side effects like audit events.
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.