idempotency-keys · git:20260720.1dc980e · 2026-07-20 · sha256 6b7cbffde6e611ae
idempotency-keys git:20260720.1dc980eA
Immutable. This exact content is served forever at /api/v1/blob/6b7cbffde6e611ae.
--- name: idempotency-keys description: Implement idempotency keys so retried requests produce one effect and one canonical response. Use when making POST endpoints retry-safe, especially payments and order creation. --- # Idempotency keys A retry after a network timeout must not charge the card twice. The client names the operation with a key; the server ensures one execution and replays one recorded response. ## Method 1. **Client generates, server scopes.** Client sends `Idempotency-Key: <uuid>` per logical operation (same key on retry, new key for a genuinely new attempt). Server scopes stored keys by authenticated caller and endpoint so keys cannot collide across tenants or routes. 2. **Reserve the key atomically before doing work.** Insert `(key, scope, request_hash, status=in_progress)` with a unique constraint in the same database as your business data. Lost race = key exists: this is the whole mechanism; check-then-insert without the constraint is a duplicate generator under load. 3. **Bind the key to the request body.** Store a hash of the payload; same key + different hash returns 422. Otherwise a client bug replays order A's response to order B's request. 4. **Record the outcome, replay it verbatim.** On completion, store status code and response body against the key; any later duplicate gets exactly that response with an `Idempotent-Replay: true` header. Concurrent duplicate while in_progress: 409 with Retry-After, do not run the work twice in parallel. 5. **Choose the failure policy explicitly.** Recommended: store the error response for deterministic failures (validation) but clear the key on infrastructure failures so a retry can succeed. Document which you do; clients build their retry loops on it. 6. **Expire pragmatically.** 24-72h TTL covers real retry windows; sweep expired rows. An unbounded key table is a slow-motion outage. ## Boundaries - GET/PUT/DELETE are idempotent by contract already; keys are for POST and other effectful non-idempotent operations. - The key dedupes the request; it does not make your downstream calls safe. Inside the execution you still need transactional effects (see transactional-outbox). - If the operation's effect happens outside your database (external charge API), execute it via that provider's own idempotency mechanism and store their reference before acking yours.