Idempotency
Updated June 3, 2026In 2020, a user tried to pay for their order on a food delivery app. The request timed out. The app retried. The user got charged twice. Both orders arrived. This is exactly the kind of bug that idempotency is designed to prevent.
Idempotency is one of those concepts that sounds abstract until you've shipped a production bug without it — then it becomes something you think about every time you design a mutation.
What Idempotency Means
An operation is idempotent if performing it multiple times produces the same result as performing it once.
The simplest example is a light switch with a "turn off" button (not a toggle). Pressing it once turns the light off. Pressing it ten more times — same result. That's idempotency.
The counterexample is a "charge $10" operation. Run it once: $10 charged. Run it twice: $20 charged. Not idempotent.
In a mathematical sense: f(f(x)) = f(x) — applying the function repeatedly has no additional effect after the first application.
Double-charge problem — timeout causes client retry, both requests processed
Why It Matters for Distributed Systems
Networks are unreliable. Any request can fail in ways that leave you uncertain whether the server processed it:
- The request was sent but the network dropped the response (the server did process it)
- The request never reached the server (the server did not process it)
- The server processed it but crashed before sending a response
From the client's perspective, these three scenarios look identical: a timeout or connection error. The client has to decide: do I retry?
If you don't retry, you risk a bad user experience (the payment "failed" even though it went through). If you do retry blindly, you risk double-charging the user.
Idempotency is what makes retrying safe.
This is why retry logic — which every resilient distributed system needs — depends fundamentally on the underlying operations being idempotent.
A client sends a payment request and receives a timeout. From the client's perspective, which of the following is true?
Idempotency key solution — Redis lookup deduplicates on second request, returns cached result
Idempotency Keys
The standard solution for non-naturally-idempotent operations is an idempotency key.
The client generates a unique key (usually a UUID) for each logical operation and includes it in the request as an Idempotency-Key header. The server stores the key and its result after processing. If the same key arrives again, the server returns the stored result without reprocessing. The client can safely retry as many times as needed — the operation executes exactly once.
POST /v1/charges
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 2000,
"currency": "usd",
"source": "tok_visa"
}If this request times out and the client sends it again with the same idempotency key, Stripe looks up that key, finds the original charge result, and returns it — no second charge is created.
Implementation Considerations
Key storage: You need a fast key-value store (Redis works well) to check for duplicate keys before processing. The lookup must happen in the same transaction as the operation, or you risk race conditions where two concurrent retries both check the key, find nothing, and both proceed.
Key expiration: Keys don't live forever. Stripe retains idempotency keys for 24 hours. After that, the same key generates a new request. Set an expiry that makes sense for your retry windows.
Scope: Idempotency keys are typically scoped per API key or per user, not globally. user_123:key_abc is a valid approach.
An idempotency key lookup must happen in the same transaction as the operation it guards. Why?
HTTP Methods and Idempotency
HTTP methods have defined idempotency semantics in the HTTP spec — knowing them helps you design better APIs.
| Method | Idempotent? | Safe? | Notes |
|---|---|---|---|
| GET | Read-only, no side effects | ||
| HEAD | Same as GET, no body | ||
| PUT | Replaces the full resource — same result each time | ||
| DELETE | Deleting something twice = it's still deleted | ||
| POST | Creates new resources — not naturally idempotent | ||
| PATCH | * | Depends on the operation |
GET is trivially idempotent — reading data doesn't change state.
PUT is idempotent because it's a full replacement. PUT /users/123 with the same body always results in the same state for user 123, no matter how many times you call it.
DELETE is idempotent — once deleted, deleting again is a no-op (you'd return 404, but the system state is the same: the resource doesn't exist).
POST is the tricky one. POST /orders creates a new order each time. This is why you need idempotency keys for POST requests that represent operations you don't want to duplicate.
Which of the following operations is NOT naturally idempotent?
PATCH depends on the operation:
PATCH /balance {"operation": "add", "amount": 10} ← not idempotent (relative update)
PATCH /users/123 {"name": "Alice"} ← idempotent (absolute set)A PATCH request is always idempotent.
Real Example: Stripe's Idempotency Keys
Stripe's payment API is the textbook example of idempotency done right. Every mutating API call supports idempotency keys. Stripe's documentation explicitly tells you:
Results are saved for at least 24 hours, and Stripe returns the same result for any subsequent retries with the same key.
Their recommendation: generate the idempotency key on the server side (not the client/browser) before initiating payment. Store it alongside your own order record. If the user hits "pay" twice or if your backend retries on a timeout, the same key ensures the charge happens exactly once.
Stripe goes further — if you retry with the same key but different parameters (e.g., a different amount), they return a 400 Idempotency-Mismatch error. This catches bugs where the client is incorrectly reusing keys.
Stripe returns a `400 Idempotency-Mismatch` error when the same idempotency key is reused with different request parameters. What problem does this behavior prevent?
Summary
Idempotency is what separates a naive API from one that's safe to operate at scale. Networks fail. Servers crash. Clients retry. If your mutations aren't idempotent, those retries become correctness bugs — double charges, duplicate records, inconsistent state. An idempotent operation produces the same result no matter how many times it runs. Idempotency keys make non-naturally-idempotent operations like payments safe to retry. GET, PUT, and DELETE are idempotent by design; POST is not. Stripe's implementation is the reference to study when designing payment flows.
Whenever you design an API endpoint that creates or modifies data, ask yourself: "What happens if this runs twice?" If the answer is "something bad", you need idempotency.
Saved on this device only
Sign in to sync progress across devices