Idempotency is the property that lets you retry an operation without making things worse. Networks fail, clients time out, and load balancers replay requests — in a world where "did it actually work?" is often unanswerable, idempotency is how you build systems that are safe to retry. It is one of the most important properties in distributed systems design and one of the most commonly underspecified.
What changed in 2026
- Idempotency keys became a standard API primitive. Stripe pioneered the pattern; now GitHub, Twilio, Adyen, and most serious financial APIs support
Idempotency-Key as a standard request header.
- Distributed sagas and workflow engines (Temporal, AWS Step Functions) bake idempotency into their execution model — activity functions must be idempotent by design.
- ULID replaced UUID v4 as the preferred idempotency key format in many codebases — ULIDs are sortable by creation time, which makes debugging and range queries easier.
- The HTTP spec clarified that idempotency is a property of the method's intended effect, not a guarantee from the server — you still have to implement it.
What idempotent means
An operation is idempotent if applying it multiple times has the same effect as applying it once:
f(f(x)) = f(x)
In API terms: sending the same request twice produces the same server state and the same response as sending it once.
GET /users/42 — idempotent: reads do not change state.
PUT /users/42 {"name": "Alice"} — idempotent: setting the name to "Alice" twice leaves it "Alice".
DELETE /users/42 — idempotent: deleting a deleted resource returns 404, but the state (absent) is the same.
POST /orders — not idempotent: two identical POST requests create two orders.
HTTP method idempotency
| Method |
Safe? |
Idempotent? |
Notes |
| GET |
Yes |
Yes |
No side effects |
| HEAD |
Yes |
Yes |
No body returned |
| PUT |
No |
Yes |
Full replace; same result each time |
| DELETE |
No |
Yes |
Subsequent calls return 404; state unchanged |
| POST |
No |
No |
Creates or triggers; each call may differ |
| PATCH |
No |
Maybe |
Depends on patch semantics (set vs increment) |
Idempotency keys for POST operations
The pattern: the client generates a unique key before the first attempt and includes it in every retry. The server stores the key and the result; on repeated requests with the same key, it returns the stored result without re-executing.
Client → POST /payments
Idempotency-Key: 01HX2J3K4M5N6P7Q8R9S0T1U2V
{"amount": 5000, "currency": "usd"}
Server: key not seen → process payment → store (key, result) → respond 201
--- network timeout; client retries ---
Client → POST /payments
Idempotency-Key: 01HX2J3K4M5N6P7Q8R9S0T1U2V (same key)
Server: key seen → return stored 201 response (no new charge)
Implementation:
import uuid
from datetime import datetime, UTC
def handle_create_payment(request):
idem_key = request.headers.get("Idempotency-Key")
if not idem_key:
return error(400, "Idempotency-Key header required")
# Check for existing result
existing = db.idempotency_keys.get(idem_key)
if existing:
return existing["response"] # replay stored response
# Process and store atomically
with db.transaction():
result = payment_service.charge(request.body)
db.idempotency_keys.insert({
"key": idem_key,
"response": result,
"created_at": datetime.now(UTC),
})
return result
Store the key and response in the same transaction as the side effect, or use a compare-and-swap operation to prevent concurrent duplicate processing.
Key generation
The client generates the key before the first call — this is critical. If the server generates it, a timed-out response cannot be retried with the same key.
import ulid
# Generate once before first attempt
idempotency_key = str(ulid.new()) # e.g., "01HX2J3K4M5N6P7Q8R9S0T1U2V"
# Use on every retry
response = requests.post(
"/payments",
json=payload,
headers={"Idempotency-Key": idempotency_key},
)
ULID is preferable to UUIDv4 for idempotency keys — it sorts lexicographically by time, making it easy to query recent keys or spot duplicates in logs.
Key expiry
Store idempotency keys for long enough to cover your retry window plus a buffer. A reasonable policy:
| Use case |
Key TTL |
| Payment API |
24 hours |
| Webhook processing |
7 days |
| Long-running workflow |
30 days |
| Synchronous user action |
1 hour |
Clean up expired keys in a background job; do not let the table grow unboundedly.
How to pick the right approach
- Read operations? Already idempotent — no key needed.
- PUT or DELETE? Naturally idempotent — design the endpoint to handle repeated calls gracefully (DELETE on missing resource returns 404, not 500).
- POST that creates a resource? Add
Idempotency-Key header support. Store key + response.
- Event processing (webhooks, queue consumers)? Deduplicate on event ID in a
processed_events table.
- Distributed saga steps? Make each activity function idempotent by checking if its postcondition is already met before acting.
Common mistakes
Generating the key on the server. The whole point is that the client can retry with the same key. Server-generated keys require a successful first response to capture the key — useless on timeout.
Not storing the response, only the key. Returning a generic "already processed" message is not idempotency — clients need the original response to continue correctly.
Allowing different payloads with the same key. If the client sends a different body with the same idempotency key, return 422 (or 409) — do not silently process a different operation.
Race conditions on concurrent requests. Two concurrent requests with the same key must not both execute. Use a database unique constraint on the key plus a transaction, or a distributed lock.
What to skip
- Relying on a unique constraint alone — it prevents double-insert but does not return the original response to the client.
- Extremely long key TTLs without cleanup — unbounded table growth degrades lookup performance.
- Using sequential IDs as idempotency keys — they are predictable and attackable. Use UUIDs or ULIDs.
FAQ
Is idempotency the same as exactly-once delivery?
No. Idempotency is a property of the operation. Exactly-once delivery is a guarantee about message delivery. Idempotent operations make it safe to use at-least-once delivery without harmful side effects.
Do I need idempotency keys for GET requests?
No. GETs are already idempotent by definition. Keys are only needed for non-idempotent operations like POST.
How do I handle the case where the same key is sent with a different body?
Return HTTP 422 Unprocessable Entity (or 409 Conflict) with a clear error message. The key is bound to the original request; a different payload implies a different intent.
What happens if two servers process the same key at the same time?
Use a database unique constraint on the idempotency key column. The second insert will fail, and that server should re-read and return the stored result.
Where to go next