Idempotency is a design property, not a retry setting
Any network call can fail after the work was done and before the answer arrived, leaving the caller uncertain whether retrying will duplicate an effect. That fact makes duplicate delivery structural rather than exceptional, and the only durable answer is receivers that tolerate it.
TL;DR: Exactly-once claims need a boundary. A database transaction can atomically record a key and its effect. An external charge needs the provider’s idempotency contract or reconciliation of an uncertain outcome; writing an intent locally does not guarantee that the charge happens exactly once.
The situation that forces it
A client sends a request. The server processes it, commits, and the response is lost, or the connection drops, or the client times out one millisecond early.
From the client's side those are indistinguishable from the server never having received anything. So the client retries, because not retrying means silently losing work that may have failed. And the server now sees the same request twice.
Acknowledgements can be lost too. Systems can provide exactly-once processing within a defined transactional boundary, but that guarantee does not automatically cover an external API call. Identify which state changes commit together and how retries are deduplicated outside that boundary.
Three mechanisms, in order of preference
A natural key with a uniqueness constraint. If the operation creates a row that has an identifier the caller already knows, let the database enforce it. INSERT ... ON CONFLICT DO NOTHING against a unique index makes the second attempt harmless with no extra machinery. This is the best option available and it is the one people skip because it feels too simple.
A caller-supplied idempotency key. Where the operation has no natural key, the caller generates one per logical operation and sends it with every attempt. The server stores the key with the result of the first execution, and any later request carrying the same key returns the stored result rather than executing again. Two details make or break it: the key must be stored in the same transaction as the effect, or a crash between them recreates the problem you were solving; and the deduplication retention window must be documented, because a retry after the key is removed may execute again. Bind the key to the caller and original operation parameters so it cannot silently refer to a different request.
A ledger, written before acting. For an external effect, record the intent durably and assign a stable operation ID before calling the provider. Pass that ID through the provider’s idempotency mechanism when available. If a charge succeeds but your worker crashes before recording completion, the local ledger cannot distinguish success from failure. Reconcile against the provider’s status API or retry with the same supported key, within its retention window; otherwise leave the operation unresolved for investigation rather than issue a blind second charge. Stripe’s idempotency contract is one concrete example: it stores the first executed result and may prune keys after at least 24 hours.
What idempotency is not
It is not a retry policy. Retries are the pressure; idempotency is what makes the pressure safe. Configuring retries without a receiver that tolerates them converts a transient failure into a duplicate charge.
It is also not the same as a request that happens to be a read. Reads are naturally idempotent, which is why the interesting cases are all writes, and why the mechanisms above all concern themselves with storing something.
The cases that break it
Non-deterministic values. A retry that generates a fresh identifier and creates another row duplicates the effect. A different response timestamp alone does not make an operation non-idempotent: idempotency concerns the intended effect on server state. Preserve operation identity across retries and store the result when the API promises response replay. HTTP semantics defines this distinction.
Partial effects. An operation writing three rows and calling two services is idempotent only if every step is, or if all of it is inside one transaction. The half that is not is the half that bites.
Ordering. Idempotent does not mean commutative. A retry that arrives after a later update can overwrite it, which is why version numbers or conditional updates belong alongside the key.
The habit that follows
Ask of any handler: what happens if this runs twice? Ask it during design rather than during the incident. In a system built from queues, retries and timeouts, running twice is not an edge case, it is Tuesday.
Self-check
A payment provider times out after you sent a charge. Can a local intent record prove no charge happened? No. Reconcile using the original provider idempotency key or payment identifier; do not issue a new logical payment merely because the response was lost.