Idempotency in APIs: The Cheap Fix for Half Your Retry Bugs
Networks drop packets. Clients retry. Without idempotency keys you charge the card twice. It's a 20-line fix for a 2 AM class of bug.
An operation is idempotent if executing it N times has the same effect as executing it once. GET and PUT are naturally idempotent. POST is not — and that's where the trouble starts.
Why it matters
Every retry is a potential duplicate. Client timeouts, proxy timeouts, mobile-network flakes — the request may have reached the server and the response may have been lost. The client cannot tell. Without idempotency, "retry on timeout" is a bug waiting to happen.
The pattern: idempotency keys
Client generates a UUID per logical operation and sends it on every retry:
POST /v1/charges
Idempotency-Key: 7d3f1e4b-...
Content-Type: application/json
{ "amount": 1999, "currency": "usd", "source": "tok_xxx" }
Server stores a hash of the request + the response, keyed by the idempotency key. On a retry it returns the cached response instead of executing again.
Storage
Two common patterns:
- Table row.
idempotency_keys(key PK, request_hash, response_body, status, created_at). TTL via a nightly cleanup or a partial index oncreated_at. - Redis with a short TTL.
SET key response NX EX 86400. Cheaper, ephemeral — fine when the client is expected to retry within hours.
The gotchas
- In-flight retry. If a retry arrives while the original is still processing, you must wait for the original to finish, not start a second one. A mutex on the key solves this.
- Key scope. Idempotency keys are scoped per endpoint + per tenant. Never reuse across unrelated operations.
- Same key, different body. Reject with 422 "Idempotency key reused with different parameters." Don't silently return the old response — that masks client bugs.
- TTL window. Long enough for any reasonable retry (24h is typical). Longer eats storage; shorter leaves gaps.
Server-side idempotency without a key
Some operations are idempotent by shape. UPDATE user SET last_login = '2026-04-24' is — you can run it twice with no harm. INSERT … ON CONFLICT DO NOTHING is. Design for this where you can; fall back to keys where you can't.
Rule
Any POST that changes money, state, or anything the user would notice as a duplicate must support an idempotency key. Everything else is a 2 AM incident waiting for a retry storm.