A request is idempotent if making it twice produces the same effect as making it once. For unreliable networks (which is to say, all networks), idempotency is essential — clients retry; without idempotency, retries cause duplicate side effects.
This page is about how to design idempotent APIs and the patterns that work in production.
Formally: f(f(x)) = f(x). Operationally: the second call has no additional effect.
Examples:
PUT /resource/123 with full state — idempotent (same final state)DELETE /resource/123 — idempotent (deleted is deleted)POST /orders creating a new order — NOT idempotent without explicit supportPOST is the typically-troublesome verb. Most state-creating operations are POST; without idempotency keys, retries duplicate.
The standard pattern: clients send a key with the request. Server records the key + response. On retry with the same key, server returns the same response without reprocessing.
POST /api/orders
Idempotency-Key: 8d4f...
{ "amount": 100.00 }
If the request is retried with the same key, the server:
Keys must be unique per logical operation:
order-{customer}-{timestamp}Clients generate the key; the server doesn't know what the client considers the same operation.
Storage requirements:
Common stores: Redis (with TTL), database table with cleanup job, dedicated idempotency service.
Schema:
CREATE TABLE idempotency (
key VARCHAR(255) PRIMARY KEY,
request_hash VARCHAR(64), -- to detect different request with same key
response_status INT,
response_body JSONB,
created_at TIMESTAMP,
expires_at TIMESTAMP
);
The request_hash lets you detect "key reuse with different payload" — a client error worth flagging.
What if two retries hit two different servers simultaneously? Both see "key not found" and both start processing.
Solutions:
Insert the key with the request before processing. If insert fails (key exists), wait or read the existing record.
-- Atomic check-and-set
INSERT INTO idempotency (key, status) VALUES (?, 'processing')
ON CONFLICT DO NOTHING;
Acquire a lock on the key before processing. Release after recording response. Other concurrent retries wait or fail.
Process both; deduplicate at storage time (transaction with constraint). Wasteful but simple.
Keys expire eventually. Common: 24 hours.
Too short: legitimate retries (network delay, client retry policy) miss the window. Too long: storage grows; old keys clutter the system.
24 hours covers most retry scenarios. Document the TTL so clients know how long they can safely retry.
POST is the verb that needs idempotency keys. PATCH is between — partial updates can be non-idempotent if the patch references current state.
Stripe popularized idempotency keys for payment APIs. Their pattern:
Idempotency-Key: <client-provided>Most payment and financial APIs follow this pattern.
The simplest case: the database itself enforces idempotency via unique constraint:
INSERT INTO orders (id, ...) VALUES (?, ...)
ON CONFLICT (id) DO NOTHING
RETURNING id;
The client provides the ID; duplicates are no-ops. Simpler than full idempotency-key storage; only works when the client generates IDs.
Some operations are naturally idempotent:
POST /orders/{id}/mark-shipped
Calling it twice has the same effect as once: status = shipped. No need for idempotency keys.
Designing operations to be idempotent by construction is often easier than retrofitting keys.