HTTP retries are safe only when keys, dedupe stores, and outbox relays agree. Use this 2026 API reference to ship both patterns correctly. Read now.
Why retries need more than a second try
Clients retry HTTP requests when networks fail, load balancers drop connections, or timeouts fire before a response arrives. A second request can create a second charge, a second order line, or a second side effect unless the server treats duplicates as the same logical operation. Idempotency is the contract that makes that safe: the client supplies a stable key for an intent, the server records the outcome under that key, and every later request with the same key returns the same result instead of re-executing the work.
That contract only holds when three pieces stay aligned. The key must identify one client intent across retries. A dedupe store must remember completed work long enough to cover the retry window. Any async work that follows the request must not fire twice when the client retries. Idempotency keys alone do not fix a pipeline that publishes events outside the same atomic boundary as the original write.
Idempotency keys and the dedupe store
Design the key as an opaque token the client generates once per logical operation and reuses on every retry of that operation. Prefer client-generated keys over server-minted ones so a timeout after the server has already processed the request does not force a new identity. Scope keys per resource or tenant so two unrelated clients cannot collide. On the first successful handling, persist the key with the HTTP status, response body or body reference, and a timestamp. On a hit, return the stored response without re-running domain logic.
Choose a store that supports atomic insert-or-read under concurrent load. Reject or queue requests whose keys are still in flight rather than running two handlers in parallel. Define a retention window that exceeds maximum client retry lifetime; expire only after that window, and document what happens if a late retry arrives after expiry. Keep response payloads small or store them by reference so the dedupe layer stays cheap to scan and purge.
The outbox so side effects match the write
Many APIs accept a request, write domain state, then publish a message or call another service. If publish happens after the database commit and the process crashes, the write exists without the message. If publish happens before commit and the write rolls back, subscribers see work that never stuck. Retries then amplify the mess: a second attempt may publish again or skip needed work depending on where the failure sat.
The transactional outbox pattern writes the domain change and an outbox row in one transaction. A separate relay reads unsent outbox rows, delivers them, and marks them sent. Consumers still need their own idempotency because at-least-once delivery can repeat messages. The point is agreement: the same logical operation that recorded an idempotency key should also be the only producer of the related outbox entry, so retries hit the dedupe store and do not insert a second outbox row.
- Accept request with idempotency key; if key is known, return stored response.
- If unknown, begin a transaction: apply domain write and insert outbox row with the same intent identity.
- Commit, then store the success response under the key.
- Relay outbox rows independently; consumers dedupe by intent or event id.
Shipping both patterns without silent double work
Treat keys, the dedupe store, and the outbox relay as one design, not three optional extras. Document which methods are idempotent, how long keys live, and what clients must do when they receive a conflict or in-progress response. Monitor orphaned outbox rows, dual handlers racing the same key, and relays stuck behind poison messages. When those three stay in agreement, HTTP retries become recovery rather than a source of duplicate side effects—and that is the bar this 2026 reference aims for when you ship both patterns correctly.