Define the effect before the retry
In algebra, an operation is idempotent when applying it again does not change the result after the first application. HTTP uses a practical version of that idea: several identical requests have the same intended effect on the server as one request. Safe methods, PUT, and DELETE carry that semantic; POST and PATCH do not carry it by default.
That definition is about the requested effect, not identical traffic or responses. A server can log every repeated request. A second DELETE can return a different status because the resource is already gone. Neither detail breaks idempotency if the business state stops changing after the first successful application.
I therefore describe the invariant before choosing the mechanism: one order is placed, one payment is captured, or one deployment is started for a particular user intent. Without that boundary, “make this endpoint idempotent” is too vague to test.
Treat a timeout as an unknown outcome
A timeout says that the caller stopped waiting. It does not say whether the server received the request, committed its transaction, or lost only the response on the way back. Retrying a non-idempotent operation can turn that uncertainty into a duplicate order or charge.
Backoff and jitter reduce pressure during a failure, but they do not make the repeated work safe. RFC 9110 allows automatic retries when the method is idempotent and warns against retrying a non-idempotent method unless the client knows its semantics are idempotent or knows the first attempt was not applied.
The useful contract is not that duplicate requests never arrive. Networks, queues, users, and application code will produce them. The contract is that every retry of the same intent can be recognized and can produce at most one durable business effect within the documented scope and retention window.
Give one intent a durable identity
For a create-style operation, I let the client generate an idempotency key before the first attempt and reuse it only for retries of that intent. Two identical request bodies can represent two legitimate purchases, so a payload hash alone is not an identity. A key alone is also insufficient: the server should scope it to the caller and operation, retain a fingerprint of the input, and reject the same key when the input changes.
The server then needs a durable record of the key, its state, and its outcome. A unique constraint can arbitrate two requests that arrive together. The first claims the operation; a completed duplicate receives the recorded outcome; an in-progress duplicate waits or receives an explicit conflict. The claim and the domain mutation should commit atomically when they share a database. If they cannot, the in-progress state needs a recovery policy rather than an indefinite lock.
Retention is part of the API contract. Once a key expires, the server can no longer distinguish a late retry from new work. The documented window should outlast every client retry and delayed-delivery path the system intends to support.
Carry the guarantee through every boundary
An HTTP key protects only the boundary that enforces it. A request can still update a row once and publish a message twice, while a consumer can process one message twice. I persist the business change and an outgoing event in one transaction, relay that event with a stable identifier, and make each consumer record the identifiers it has already applied. If a downstream API supports idempotency, I derive and forward a stable key for that specific side effect.
Tests should interrupt the workflow at its awkward points: after the commit but before the response, during two concurrent attempts, after a worker restarts, with a reused key and changed input, and just beyond the retention window. Observability should count attempts separately from logical operations so retries remain visible without looking like extra business activity.
Idempotency is not a blanket “exactly once” switch. It is a scoped promise about which effect, for which caller and operation, over what period. When those terms are explicit, retries become a recovery tool instead of a wager.
References
- RFC 9110 — HTTP Semantics, Idempotent Methods
- IETF Internet-Draft (expired work in progress) — The Idempotency-Key HTTP Header Field
- Stripe API — Idempotent requests
- Amazon EC2 — Ensuring idempotency in API requests
- Google Cloud Storage — Retry strategy
- PostgreSQL — INSERT and ON CONFLICT
- Microsoft Azure — Transactional Outbox pattern
- Microsoft Azure — Idempotent Consumer pattern