1. What is Idempotency?
A mathematical function f is idempotent if f(f(x)) = f(x) — applying it multiple times produces the same result as applying it once. In APIs, an operation is idempotent if making the same request N times has the same effect on server state as making it once.
This matters because distributed systems have three outcomes for any request: success, failure, or unknown. A network timeout does not tell you whether the server processed the request. Without idempotency, you cannot safely retry — you might create duplicate orders, duplicate charges, or duplicate records.
2. HTTP Method Idempotency
| HTTP Method | Idempotent? | Safe (read-only)? | Notes |
|---|---|---|---|
| GET | Yes | Yes | Read-only, no state change |
| HEAD | Yes | Yes | Same as GET but no response body |
| OPTIONS | Yes | Yes | Returns allowed methods, no state change |
| PUT | Yes | No | Sets resource to specific state; repeating has same result |
| DELETE | Yes | No | First call deletes; subsequent calls return 404 but state unchanged |
| POST | No (by default) | No | Each call may create a new resource; requires idempotency key to make safe |
| PATCH | No (by default) | No | Incremental update; repeating may compound (e.g. increment counter twice) |
DELETE Idempotency Nuance
DELETE is idempotent in terms of server state — after N calls, the resource is deleted. But the HTTP response code changes: the first call returns 200 or 204, subsequent calls return 404. Some strict REST purists argue this breaks idempotency. In practice, clients should treat 404 on DELETE as success — the desired state (resource does not exist) has been achieved.
3. The Problem: Unsafe POST Retries
POST is not idempotent by design. Consider a payment creation API:
Client ─── POST /payments {amount: 100, card: xxxx} ───▶ Server
│
DB write succeeds
Payment charged
│
Network timeout!
│
Client <─── ??? (never received 200 response) Response lost
Client retries:
Client ─── POST /payments {amount: 100, card: xxxx} ───▶ Server
NEW payment created
Customer charged TWICE!
The client has no way to know if the first request succeeded. If it retries naively, the customer is charged twice. Idempotency keys solve this.
4. Idempotency Keys
An idempotency key is a unique identifier generated by the client before making the request. The client includes it in the request header. The server stores the key along with the result. On a retry with the same key, the server returns the stored result without re-executing the operation.
Client generates: idempotency_key = UUID("a3f8-bc12-...")
First request:
Client ── POST /payments
Idempotency-Key: a3f8-bc12-...
{amount: 100} ──▶ Server processes, stores:
key a3f8-bc12 → {payment_id: 789, status: success}
Returns: 200 {payment_id: 789}
Network timeout — client retries with SAME key:
Client ── POST /payments
Idempotency-Key: a3f8-bc12-...
{amount: 100} ──▶ Server: key a3f8-bc12 already exists!
Returns CACHED result: 200 {payment_id: 789}
NO new payment created ✓
5. Server-Side Idempotency Key Implementation
6. Stripe's Idempotency-Key Header
Stripe is the industry benchmark for payment API idempotency. Every POST request to the Stripe API accepts an Idempotency-Key header. Stripe stores the key and result for 24 hours. Key behaviours:
- Keys are scoped to your API key — two different accounts can use the same UUID without conflict
- If you send the same key with different request body, Stripe returns 422 (the key is locked to the original request parameters)
- Keys expire after 24 hours — after that, the same UUID can be reused
- Stripe recommends using UUID v4 generated fresh for each distinct operation
- Stripe returns the original HTTP status code and response body on a replay, including error responses — if the original request failed, the cached failure is returned
7. Database Upsert Pattern for Idempotent Writes
For idempotent database writes, use a natural unique key or idempotency token with an upsert (insert-or-ignore / insert-on-conflict-do-nothing):
Idempotency Key Scope Matters
Idempotency keys must be scoped to a specific operation. Never reuse the same key for different operations. A key that worked for "create payment" should never be reused for "create refund." The server must also validate that the request body for a retried key matches the original — Stripe rejects requests where the key matches but the body differs, preventing accidental misuse of a key from a previous operation.
8. Making Non-Idempotent Operations Idempotent
You can retrofit idempotency onto any operation with the following pattern:
- Require clients to send a unique
Idempotency-Keyheader on all write operations - Before executing, check if the key exists in your store (Redis or DB table)
- If the key exists: return the stored result (do not re-execute)
- If the key is new: execute the operation, store the result with the key, return the result
- Handle concurrent requests for the same key with a distributed lock
- Expire keys after a reasonable window (24 hours to 7 days)
This pattern is applicable to payment creation, order placement, email sending, SMS dispatch, and any operation that must not be duplicated on retry.
How We Research and Update This Guide
We test the underlying formula or workflow, compare outputs with reliable references, and revise examples whenever the page content changes.
- The workflow or formula is tested directly in the tool and compared against independent reference examples.
- Examples are kept practical so readers can verify the result without hidden assumptions.
- Pages are revised whenever the interface, calculation flow, or surrounding guidance materially changes.
Frequently Asked Questions — Idempotent APIs
An API operation is idempotent if making the same request multiple times produces the same result as making it once. The server state after N identical requests is the same as after one request. For example, PUT /users/123 with the same body will always result in the same user state — making it 10 times is the same as making it once. This property is critical for safe retries in distributed systems where requests can fail mid-flight.
GET, HEAD, PUT, DELETE, OPTIONS, and TRACE are all idempotent by HTTP specification. GET and HEAD never modify state (also "safe"). PUT sets a resource to a specific state — calling it 10 times with the same body leaves the resource in the same state. DELETE removes a resource — deleting an already-deleted resource returns 404 but does not change server state further. POST is NOT idempotent by default — each call typically creates a new resource.
An idempotency key is a unique token generated by the client (usually a UUID) and sent with a request (typically in a header like Idempotency-Key). The server stores a mapping of key → result. If the same key is received again, the server returns the cached result without re-executing the operation. This makes non-idempotent operations (like POST to create a payment) safe to retry — if the network drops and the client retries, the server recognizes the duplicate key and returns the original result.
Stripe accepts an Idempotency-Key HTTP header on all POST requests. When you create a payment, you generate a UUID and include it as the Idempotency-Key. Stripe stores this key with the result for 24 hours. If your server crashes after sending the request but before receiving the response, you can safely retry with the same key — Stripe will return the original payment result instead of creating a duplicate charge. This is the industry standard pattern for payment APIs.
Store idempotency keys for at least as long as your client retry window. Stripe stores keys for 24 hours. For long-running workflows, store them longer — 7 days is common for e-commerce order workflows. Use Redis with a TTL for efficient storage: SET idempotency:{key} {result} EX 86400. If you use a database table, run a periodic cleanup job to purge expired keys. The storage cost is minimal — a UUID (36 bytes) plus the cached response.
Use an upsert (INSERT ... ON DUPLICATE KEY UPDATE or INSERT ... ON CONFLICT DO UPDATE) with a natural unique key or idempotency token as the uniqueness constraint. For example, when creating an order, include the client-generated order_token in the INSERT with a UNIQUE constraint. If the insert is retried, the UNIQUE constraint prevents a duplicate row, and the upsert returns the existing row. This is the database-level idempotency pattern — it survives application restarts and distributed retries.