arthrop0d

Brief articles on varied topics.

Idempotency Keys and Safe Request Retries

api designsoftware engineering

An idempotency key is a client-generated identifier that tells a server, “Treat repeated requests with this identifier as the same operation.” The server stores the operation’s result under that key and returns the stored result when the client retries.

This makes a request idempotent: performing it more than once has the same externally visible effect as performing it once.

The failure it solves

Suppose a client sends a request to create a payment, order, or account. The server processes it, commits the change, and sends a response. Then the network fails before the client receives that response.

From the client’s perspective, the request is ambiguous:

Retrying is usually the right response to a lost connection. Without an idempotency key, however, the server sees the retry as a new request. It might charge the customer twice or create two orders.

An idempotency key gives the server a way to recognise that the retry represents the same intended operation, even though it arrives as a separate HTTP request.

A timeout tells the client that it does not know the result. It does not tell the server to undo the work.

What the client sends

The client creates a new, sufficiently unique key for each operation it wants performed once. It sends that key with the request, often in an HTTP header such as Idempotency-Key:

POST /payments
Idempotency-Key: 7f2e9c1d-...

The same key must be reused for retries of that operation. A new payment must receive a new key, even if its amount and other fields happen to be identical.

The key should be scoped to the relevant account or authenticated client. Otherwise, two unrelated customers using the same value could accidentally collide. Servers commonly enforce uniqueness for a combination such as (account_id, idempotency_key), rather than for the key alone.

What the server records

When the server receives a keyed request, it looks for a previously stored record. That record commonly contains:

The fingerprint is a compact representation of the request contents. It prevents a client from accidentally reusing a key for a different operation. For example, if the first request creates a $20 payment and a later request with the same key asks for $200, the server should reject the second request rather than return the first payment’s result. A typical response is a conflict error explaining that the key was already used with different input.

The server needs a uniqueness constraint in its durable store—a rule that prevents two records with the same scope and key. It must also create the record and perform the operation in a coordinated way. Merely checking for a key and then inserting it leaves a race: two simultaneous requests can both observe that the key is absent and both continue.

The request lifecycle

A typical implementation follows this sequence:

  1. Validate the key and request. Reject a missing key when the operation requires one, and reject malformed input before recording a result.
  2. Look up the key. If a completed record exists, compare the request fingerprint. For a match, return the stored status and response without performing the operation again.
  3. Reserve the key. If no record exists, create a record marked as processing. The uniqueness constraint makes this reservation atomic—indivisible from other requests trying to reserve the same key.
  4. Perform the operation. Create the payment, order, or other resource.
  5. Store the result. Save the final status and response with the idempotency record.
  6. Respond. Send the result to the original caller.

If another request arrives while the first is processing, the server must define its behaviour. It can wait for the first request to finish, return a response meaning “the operation is still in progress,” or reject the concurrent request for now. It must not start the operation a second time.

For a database-backed operation, the reservation, business change, and result record can often be committed in one database transaction. A transaction is a group of database changes that commits as one unit or rolls back as though none happened. This prevents a committed business change from existing without its replayable result.

That coordination has limits. If processing calls an external system that uses a separate database—for example, a payment provider—the local transaction cannot automatically include that provider. The external operation must support its own idempotency mechanism, or the system needs a design such as a durable work record and carefully coordinated retry handling. An idempotency key is not a magic guarantee that every downstream service will ignore duplicates.

Replaying the result

On a retry, the server should return the stored outcome, including meaningful failure outcomes. If the original operation completed with a validation or business error, repeating the request should not unexpectedly run it again. The exact response status and body may be replayed, subject to the API’s contract.

This is why the server records more than “key seen.” A boolean would tell it that a request happened, but not whether it succeeded, what resource it created, or what response the client should receive.

A record might conceptually move through these states:

State Meaning
Processing One request owns the operation and has not finished.
Completed The result is stored and can be replayed.
Failed The server has recorded a terminal result, if the API treats that failure as final.

The handling of crashes during Processing needs an explicit policy. A lease or expiration can allow another attempt after the owner is presumed dead, but reclaiming a record safely is difficult if the original request may still be running. Systems often use database locks, transaction timeouts, or a background worker with an ownership token.

Retention is part of correctness

Servers usually keep idempotency records for a limited period to control storage costs. That period must cover the client’s realistic retry window. If the record expires while a client is still retrying, the next retry can create a duplicate effect.

Clients should therefore reuse a key only within the server’s documented retention period and create a new key for a genuinely new operation. They should also log the key: it is useful for correlating the original request, retries, and any incident report.

You may have encountered this mechanism in an API error such as “idempotency key already used,” a configuration option for key retention, or a log line showing a “replayed idempotent response.” The central idea is simple: the key identifies the intended operation, and the stored result turns an uncertain retry into a safe lookup.

← All articles