Idempotency
Date: 2026-08-17
An operation safe to repeat: doing it twice leaves the same state as doing it once. Every system that retries needs it, and networks guarantee that everything eventually gets retried — including by users pressing the button again.
An idempotent operation produces the same resulting state however many times it is applied.
IDEMPOTENT NOT IDEMPOTENT
set stock to 5 decrement stock by 1
mark order as paid add £10 to balance
delete order 42 create an order
set email to X append to a list
Note the pattern: absolute assignments are idempotent, relative changes aren’t. That’s the single most useful heuristic here.
Why it’s unavoidable
A client that doesn’t receive a response cannot tell these apart:
CLIENT SERVER
POST /orders ──────────→ order created ✓
✗ timeout ← response lost
client sees: failure
server sees: success
The client must retry — it has no other option — and without idempotency that retry creates a second order. This isn’t an edge case; it’s the normal behaviour of mobile networks, and it’s why duplicate orders exist at every retailer.
Idempotency keys
The standard mechanism, and the one payment providers require:
1 client generates a unique key per
ATTEMPT (a UUID), not per retry
2 POST /orders
Idempotency-Key: 3f2a...
3 server checks: seen this key?
no → do the work, store
key → result
yes → return the STORED result,
do nothing
4 retries carry the same key
→ one order, correct response
The key must be generated once, when the user acts — not regenerated per attempt, which would defeat the whole thing.
Two details that get missed:
- Store the response, not just the key. A retry should return the original result, not a bare “already done”
- Expire keys on a stated window — 24 hours is common. Keeping them forever is a growing table with no upside
HTTP’s contract
The method semantics say which are idempotent, and this is what makes proxies, browsers and load balancers safe to retry:
GET safe + idempotent
HEAD safe + idempotent
PUT idempotent (full replacement)
DELETE idempotent (second one: 404
or 204, same state)
POST NOT idempotent ← needs a key
PATCH depends on the payload
“Safe” means no state change at all. A GET that mutates something breaks prefetching, caching and every crawler assumption — HTTP Semantics.
PATCH is the subtle one. {"status": "paid"} is idempotent; {"stockDelta": -1} isn’t. Same method, different answer, decided by the body.
Where it’s required
- Payments. Every provider mandates an idempotency key. Charging twice is the worst bug in commerce
- Webhooks. Delivered at least once and unordered. Every handler must tolerate duplicates — deduplicate on the event ID — Reference - AEM
- Message queues. Same guarantee, same requirement
- Analytics events. A retried event is an inflated conversion count, which is why event IDs and deduplication windows exist — Double Counting
- Database migrations. A migration that fails halfway must be safe to run again — Database Migrations
- Deploys and infrastructure. The whole appeal of declarative configuration is that applying it twice is a no-op
Designing for it
- Prefer absolute over relative.
quantity = 3survives retries;quantity += 1doesn’t - Let the client supply the ID. If the client generates the order ID, a retry with the same ID is naturally deduplicated by the primary key
- Unique constraints in the database are the only reliable dedup. An application-level check has a gap — Race Conditions
- Make delete tolerant. Deleting something already gone should succeed, not error
- Design the retry policy alongside the endpoint, not afterwards. Retries with exponential backoff plus jitter, and a cap — Error Handling Strategies
The related idea
At-least-once delivery plus idempotent handling equals exactly-once processing. Genuine exactly-once delivery is effectively unavailable in a distributed system, so this pairing is how the industry gets the practical equivalent — and it’s why “make the handler idempotent” is the standard answer to almost every duplicate-processing question.