A client can send the same logical operation more than once even when it intended one effect. A timeout after POST /payments leaves an ambiguous boundary: the server may have committed the payment while the client received no response. Retrying restores delivery, but an ordinary retry can create a second payment. An idempotency key changes the interface by giving repeated attempts a stable request identity.

The key is not a substitute for transactionality, and it does not make every operation intrinsically idempotent. It creates a protocol between client and server: attempts carrying the same key are treated as candidates for the same logical operation. The server still needs rules for request equivalence, concurrent arrival, persistence lifetime, failure recovery, and response replay.

The key identifies an operation, not a transport attempt

A retry often creates a fresh TCP connection, HTTP request object, trace span, or process execution. Those transport identities cannot represent the logical operation because each attempt can differ at that layer.

An application-generated key remains stable across attempts:

POST /payments
Idempotency-Key: 7c7c2f0e-6f39-4d57-8cb4-95ec3d2c4f51
Content-Type: application/json

{"account":"A17","amount":2500,"currency":"USD"}

If the response is lost, the client sends the same operation with the same key. A new payment initiated later receives a different key even if its payload happens to be identical.

This distinction avoids using payload equality as the sole identity rule. Two legitimate operations can have identical bodies. Conversely, a reused key with a different body signals a protocol conflict rather than a second valid operation.

Request matching closes a dangerous reuse case

A server that stores only key -> response can silently accept accidental key reuse for a different request. The safer record binds the key to enough request information to detect that mismatch.

A conceptual record can contain:

key
request_fingerprint
state
status_code
response_body
created_at

The fingerprint may cover the HTTP method, route identity, and a canonical representation of fields that define the operation. Exact composition is an API design decision. Headers or fields that do not affect operation semantics need not be included, while omitting a semantically relevant field can make distinct requests appear equivalent.

On a repeated key, the server compares the incoming request with the stored fingerprint. A mismatch should produce a conflict response rather than replaying a result created for different input.

Canonicalization also needs a defined contract. Hashing raw JSON bytes makes whitespace and object-member ordering significant even when the application treats them as equivalent. Hashing a parsed canonical form can avoid that distinction, but the canonicalization rules then become part of the server implementation.

Concurrent duplicates require an atomic claim

Sequential duplicate detection is insufficient. Two requests with the same key can arrive at nearly the same time:

request A ---- check: absent ---- execute
request B ---- check: absent ---- execute

Both requests observed absence before either stored a result. The idempotency table prevented nothing.

The server needs an atomic transition that claims the key. A database uniqueness constraint is a common primitive:

INSERT INTO idempotency_records (key, request_fingerprint, state)
VALUES (?, ?, 'in_progress');

with a unique constraint on the key, or on a scoped pair such as (tenant_id, key). Exactly one concurrent insert can succeed. A conflicting request then reads the existing record and follows the API’s policy for an operation that is still in progress.

Possible policies include waiting for completion, returning a retryable status, or coordinating through another synchronization mechanism. The important property is that duplicate execution cannot begin merely because two absence checks raced.

The scope of uniqueness matters as well. A globally unique key space can be unnecessary and can leak coupling across tenants. Scoping by authenticated account or tenant often matches the operation boundary more closely.

The business effect and idempotency record must share a failure model

Claiming a key before executing the operation introduces another boundary. The process can crash after marking the record in_progress. It can also commit the business mutation and crash before storing the replayable response.

If the business mutation and idempotency record live in the same transactional database, one transaction can often couple the effect with the final idempotency state:

begin transaction
  verify or claim key
  apply business mutation
  store completed result
commit

That arrangement can prevent a committed business effect from existing without the corresponding completed record, subject to the database transaction semantics.

The boundary becomes harder when the operation calls an external service. A local transaction cannot atomically commit both a local row and an unrelated remote API call. In that case, the system needs an explicit distributed failure strategy. The downstream service may accept the same idempotency key, or the caller may persist an operation state machine that can reconcile ambiguous outcomes. An idempotency table alone cannot create an atomic commit across independent systems.

Response replay is part of the observable contract

After an operation completes, a duplicate request normally should not execute the business action again. It should receive a response representing the stored outcome.

That can mean persisting the original status code and response body:

key -> 201, {"payment_id":"p_8042","status":"accepted"}

Recomputing a response from current database state is not always equivalent. State may have changed since the original operation. A payment accepted earlier might now be settled or refunded. Returning today’s representation changes the observable result of the retry.

APIs can intentionally choose another policy, but it should be explicit. Storing the original response gives strong replay semantics at the cost of storage and possible sensitivity of persisted response data.

Error handling also requires a boundary. Validation failures that occur before an operation is accepted may be left unstored so a corrected request with a new key can proceed. Failures after execution begins can be materially different because retrying may repeat a partially completed effect. The API must define which outcomes become durable idempotency records.

Expiration limits the guarantee

Idempotency records cannot usually be retained forever. A retention window bounds both storage cost and the duration of the retry guarantee.

Suppose records expire after 24 hours. A duplicate delivered within that window can be recognized. The same key arriving after deletion can look new and execute again. The API therefore does not offer timeless exactly-once execution; it offers duplicate suppression within a stated key-retention boundary.

Expiration also affects clients. A retry queue that can remain offline longer than the server’s retention period cannot assume an old key will still suppress duplicates. Long-lived workflows may need durable operation identifiers in the domain model rather than relying only on an HTTP idempotency cache.

Deletion should respect active operations. Expiring an in_progress record solely from its creation timestamp can permit a second execution while the first is still running. Cleanup policy needs to account for operation state and the maximum credible execution or recovery interval.

Idempotency is narrower than exactly-once delivery

Networks can duplicate, delay, or lose messages, and clients can lose responses. An idempotency-key protocol addresses one specific consequence: repeated attempts can be mapped back to one logical operation while the server retains the relevant record and all participating effects obey the required failure contract.

It does not force the network to deliver once. It does not make external side effects transactional. It does not repair a client that generates a new key for every retry. It also does not protect requests after the retention boundary has passed.

The useful guarantee is therefore stated at the API boundary: for a defined key scope, request-equivalence rule, retention interval, and execution path, repeated matching requests reuse one operation record instead of independently starting the business action. That narrower statement exposes the actual mechanisms that must remain correct under retries and crashes.