Idempotency Keys Make Retried Writes Safe to Repeat

A client can lose the response to a successful write. The connection may close after the server commits a payment, creates an order, or schedules a job but before the response reaches the caller. From the client’s perspective, failure and success can look identical.

Retrying blindly is dangerous for operations with non-idempotent effects. Sending the same POST twice may create two resources or charge twice. Refusing to retry leaves the caller with an ambiguous outcome.

An idempotency key gives repeated attempts a stable identity. The client generates a key for one logical operation and sends that same key on every retry. The server records the outcome associated with the key and can return that outcome instead of executing the effect again.

The key identifies an operation, not a transport attempt

A new transport attempt must reuse the existing key when it represents the same intended write.

attempt 1:
POST /payments
Idempotency-Key: 7f3a...

connection closes before response

attempt 2:
POST /payments
Idempotency-Key: 7f3a...

The second request is not a new payment. It is another attempt to obtain the result of the payment identified by 7f3a....

A genuinely new payment requires a new key. Reusing a key across unrelated operations collapses distinct writes into one identity and can return a result that belongs to an earlier request.

This makes key lifecycle a client-side contract as well as a server-side mechanism.

Deduplication must be atomic with operation ownership

A server cannot safely implement idempotency as a loose check followed by an insert:

if key does not exist:
    perform side effect
    insert key

Two concurrent requests can both observe that the key is absent and both perform the side effect.

The system needs an atomic transition that grants one request ownership of the key. A database uniqueness constraint is a common building block:

INSERT INTO idempotency_records (scope, key, status)
VALUES ('account-42', '7f3a...', 'in_progress');

A unique index on (scope, key) allows only one insert to succeed. Other requests can inspect the existing record and follow the policy for an operation already in progress or already completed.

The exact transaction boundary depends on where the business effect lives. If the effect and idempotency record share one transactional database, they can often be committed together. If the effect crosses an external service boundary, a local uniqueness constraint alone cannot make the remote side effect atomic.

Request parameters belong to the stored identity

A key should not silently authorize arbitrary payloads. If a caller sends the same key with different parameters, treating both requests as equivalent can hide a client defect or return a result for the wrong operation.

A practical record stores a canonical fingerprint of the request fields that define the operation:

key:          7f3a...
fingerprint:  sha256(canonical operation fields)
status:       completed
response:     201 + resource reference

On a retry, the server compares the incoming fingerprint with the stored value. A mismatch should produce an explicit conflict rather than executing a second operation or returning an unrelated result.

Canonicalization must be stable. Hashing raw JSON bytes is often unsuitable because semantically equivalent JSON can differ in whitespace, object member order, or serialization details. The service should define the fields and encoding that participate in the fingerprint.

In-progress requests need a defined response policy

A retry can arrive while the first attempt is still running. At that point there is no completed response to replay.

Several policies are valid:

A. wait for the first attempt, then replay its result
B. return a conflict or retryable status while work is in progress
C. expose operation state through a separate status resource

The choice depends on request duration, server architecture, and client timeout behavior. What matters is that a second request does not start the same effect independently.

Waiting can simplify client behavior for short operations but consumes request capacity. Returning an in-progress response keeps request handling bounded but requires the client to retry or poll according to a documented contract.

The stored result must include failures deliberately

Not every failure has the same replay semantics. A validation error produced before any side effect is usually safe to return consistently. A transient infrastructure failure before operation ownership may permit a fresh execution. A failure after an external side effect may leave the operation outcome uncertain.

The idempotency state machine should distinguish these cases instead of treating every non-2xx response identically.

For example:

reserved -> executing -> completed
                    \
                     -> indeterminate

An indeterminate state is useful when the service cannot prove whether a downstream effect occurred. Automatically running the effect again would convert uncertainty into a possible duplicate.

The recovery path may query the downstream system by its own operation identifier, reconcile asynchronously, or require operator intervention for rare cases. The correct action follows from the downstream API’s guarantees.

Retention defines the retry window

Idempotency records cannot always be stored forever. Services commonly expire them after a documented retention period.

Expiration creates a semantic boundary. Once a record is removed, the same key can no longer prove that an earlier operation existed. A very late retry could therefore execute again unless another durable business constraint prevents it.

The retention period should exceed the maximum retry horizon the API promises to support. Client SDKs should not keep retrying a key beyond that contract as if deduplication were permanent.

For high-value operations, the business object may provide a longer-lived uniqueness rule. An order reference, transfer identifier, or ledger entry can remain unique even after the transport-level idempotency record expires.

Scope keeps unrelated callers from sharing a key space

A random key has a low collision probability, but server-side scope is still important. The effective identity is often a tuple such as:

(tenant_id, endpoint_family, idempotency_key)

Scoping prevents one tenant from occupying a key that another tenant happens to use and allows the same opaque value to be valid in independent namespaces.

The scope must come from authenticated server-side context where possible. Trusting a caller-supplied tenant identifier without authorization can turn the deduplication store into a cross-tenant interference mechanism.

Keys should also have bounded length and a restricted representation so they cannot create excessive index entries or pathological storage costs.

Idempotency does not provide exactly-once execution by itself

The phrase “exactly once” hides several separate guarantees. An idempotency key can make repeated API submissions converge on one logical operation, but it does not automatically make every downstream action execute exactly once.

If the service writes a database row and publishes a message in separate steps, a crash can still occur between them. Transactional outbox patterns, downstream deduplication, or operation identifiers may be needed at later boundaries.

The useful guarantee is narrower and easier to verify: for a defined scope and retention window, repeated requests carrying the same key and equivalent operation parameters do not independently create the protected effect.

That guarantee should be stated in API documentation with the retention period, conflict behavior, in-progress behavior, and response replay rules.

Observability should follow the logical operation

Retries can make request-level metrics noisy. Five HTTP attempts may represent one logical write.

Logs and traces should carry the idempotency key or a safe derived identifier so operators can correlate attempts without treating each one as an independent business action. Metrics can separately count transport attempts, deduplicated retries, key conflicts, in-progress collisions, and completed logical operations.

Raw keys may be sensitive if clients embed business identifiers despite guidance to use opaque random values. Logging policy should therefore avoid assuming every key is harmless metadata.

A well-defined idempotency contract turns an ambiguous network retry into a deterministic protocol decision. The client preserves the identity of the intended operation, while the server atomically claims that identity, validates repeated parameters, and reuses the recorded outcome. The result is not a universal exactly-once primitive; it is a precise boundary that makes retried writes safe where the service can enforce it.