Idempotency Keys and Retry Semantics
How to make mutation retries safe by binding idempotency keys to parameters, results, and dedupe windows.
Retries turn partial failure into duplicate execution unless the operation is designed for them. Idempotency makes repeated requests with the same intent produce one logical effect.
Idempotence
An operation is idempotent when applying it more than once has the same logical effect as applying it once:
\[f(f(x)) = f(x)\]GET is normally idempotent because it does not create a new resource. POST /charges is not idempotent unless the API adds an idempotency mechanism.
Request key contract
An idempotency key identifies one client intent. The server stores the first completed result for that key and returns the same result to later retries.
A robust record stores:
| Field | Purpose |
|---|---|
| key | client retry identity |
| parameter hash | prevents key reuse with different input |
| operation name | avoids cross-endpoint collision |
| status | in progress, completed, failed |
| response summary | stable replay result |
| resource id | reconciliation path |
| expiry | bounded storage growth |
Parameter binding matters. If the same key is reused with different parameters, accepting it can hide a client bug and corrupt intent.
Race handling
Two identical requests can arrive concurrently. The server needs an atomic claim:
INSERT INTO idempotency_keys (key, param_hash, status)
VALUES (?, ?, 'in_progress')
ON CONFLICT DO NOTHING;
Only the request that creates the claim executes the mutation. Others wait, poll, or receive a conflict response depending on API semantics.
The mutation and idempotency result should commit atomically, or recovery must repair incomplete records. A common pattern is one database transaction containing the idempotency row, mutation, and response metadata.
Retry semantics
Retries need budgets and backoff. A safe mutation can still overload the service if every client retries aggressively.
Classify failures:
| Failure | Retry behavior |
|---|---|
| validation error | do not retry without changed input |
| auth failure | retry only after credential refresh |
| timeout before result known | retry with same idempotency key |
| 409 in progress | retry after delay or poll status |
| overload | retry with backoff and budget |
Practical checks
- Idempotency keys are required for externally retried mutations.
- Keys are high entropy and contain no secrets.
- Same key plus different parameters is rejected.
- Concurrent same-key requests produce one mutation.
- Result replay includes failed results when the operation began and failed deterministically.
- Expiry policy matches the maximum client retry window.
- Metrics track key conflicts, replays, mismatched parameters, and in-progress age.
Design guidance
Make retries safe at the API contract, not at the client callsite. If the operation can commit side effects before the client sees the response, require an idempotency key or expose a reconciliation read path.