Case: Stripe's Idempotency Keys
An idempotency key tells a server which intended operation a request belongs to. That matters deeply in payment systems: when the server completes work but its reply never reaches the caller (due to a network timeout), another attempt should recover the result without charging the customer twice.
Stripe's implementation of idempotency is the industry gold standard for building robust distributed systems where network failures are inevitable.
The Problem: The Lost Response
Imagine a client sends a POST /v1/charges request to Stripe to charge $50. Stripe processes the payment, deducts the money, and sends back an HTTP 200 OK. But right before the response reaches the client, the client's wifi drops.
The client's code receives a timeout. The client doesn't know if the request never reached Stripe, or if Stripe processed it but the response was lost. If the client retries the request, the user might be charged another $50. If the client doesn't retry, the user might not get their goods, even though they were charged.
The Solution: Idempotency Keys
Stripe supports client-supplied Idempotency-Key headers for eligible mutating requests. One key names one intended operation, so a retry must reuse both the key and the same parameters; a new checkout attempt needs a new key.
POST /v1/charges
Idempotency-Key: a1b2c3d4-e5f6
Amount: 5000
When Stripe receives a request, it checks its database (or a fast distributed cache) to see if it has seen this key before.
- First Request: The service validates and begins one operation under the key. It retains the resulting status and response according to the provider's documented idempotency policy.
- Network Fails: The client does not get the response and retries with the same key and parameters.
- Second Request: If the first operation has a retained result, the service returns it instead of starting another charge. A concurrent in-progress request or changed parameters must be handled explicitly rather than treated as an ordinary replay.
Where to Store the Keys
An API can atomically claim an idempotency key with its local operation record and reject the same key with changed parameters. It cannot place an external card-network effect in that database transaction. After a timeout, the provider may have accepted the charge even though the local response record is incomplete, so recovery needs the same downstream operation identity, an explicit unknown state, provider lookup or webhook evidence, and reconciliation.
Stripe may remove keys once they are at least 24 hours old to save storage space, as retries usually happen within minutes, not days.
Worked example: a charge with a lost response
- Client generates key
k-7f3a(a V4 UUID) for "charge order 991 for 50.00" and stores it with the order. - Request 1 arrives. The server inserts
(key=k-7f3a, request_hash=h, state=started)in the same database transaction that creates the local charge record. The unique constraint on the key makes this claim atomic. - The server calls the card processor, passing its own stable reference derived from the charge id. The processor approves. The server stores the response body and sets
state=finished. - The client's connection drops before the response arrives.
- Request 2, same key and same parameters: the server finds
state=finishedand replays the stored response. No second authorization.
Every case the server must handle
| Situation on arrival | Correct response |
|---|---|
| Key never seen | Claim it atomically and execute |
| Key finished, same parameters | Replay the stored status and body, even if it was an error |
| Key finished, different parameters | Reject: the key is being misused |
| Key in progress (concurrent request) | 409 Conflict; client retries later |
| Key started, then server crashed mid-way | Resume from the last recorded recovery point, or query the processor using the stable reference, then finish |
| Key older than retention window | Treated as new; Stripe documents removal after at least 24 hours |
Designing the recovery points
Brandur Leach's widely cited write-up of Stripe-like keys splits a request into atomic phases separated by recovery points (for example started, charge_created, finished). Each local phase commits in one database transaction together with the new recovery point. Foreign calls happen between phases and must themselves be idempotent, so re-executing from a recovery point is safe.
Client-side rules
- Retry only on network errors, timeouts and 5xx or 409 responses; use exponential backoff with jitter.
- Never reuse a key for a genuinely new attempt, such as a second checkout.
- Persist the key before sending, so an app restart still retries with the same key.