A technician's hand organizes two blue Ethernet cables connected to adjacent ports in a server-room network switch.

Make Duplicate Requests Harmless With Idempotency Keys

A customer submits an order. The server commits it, but the connection closes before the confirmation reaches the browser. The browser retries. Without a way to identify the first attempt, the backend may create a second order for the same intent.

The timeout did not prove failure. It proved that the client lacked a conclusive response. An idempotency key gives the client and server a shared identity for that one operation, so uncertainty does not automatically become another business action.

Follow the request through the failure

In this hypothetical order flow, the client generates a key when the customer confirms a particular order. It preserves that key while retrying the same order after a connection failure. A genuinely new order receives a new key, even if its contents happen to match the first.

The server associates the key with the authenticated caller and operation. It records enough information to recognize the request and recover its outcome. A completed retry can then receive the existing order identifier instead of creating another order.

HTTP’s definition of idempotency concerns the intended server effect of repeated identical requests. It does not require every response to have identical wording or status. POST is not inherently idempotent, so this retry behavior must be an explicit contract implemented by the application.

Define what “the same request” means

Scope the key to a caller or tenant and an operation, rather than treating a token as globally meaningful. Authenticate retries normally. Knowledge of a key must not become permission to retrieve another customer’s result.

Store a comparison representation of the relevant request parameters, such as a hash of a carefully normalized payload. Define normalization deliberately: a changed delivery address should not disappear from the comparison, while irrelevant serialization differences should not accidentally create a mismatch.

If a caller reuses the key with different parameters, return a documented mismatch error. Silently replaying an old result would conceal the changed intent. Silently accepting the new parameters would defeat the key’s purpose.

Let the database arbitrate concurrent arrivals

A sequence that checks whether a key exists and then inserts it has a race: two requests can both finish the check before either inserts. Give the storage layer an enforceable uniqueness rule instead.

For a simple relational design, require non-null values and a unique combination of caller, operation, and key. PostgreSQL supports composite unique constraints. The application must handle the conflict path rather than treating a uniqueness error as permission to run the business action again.

When the order and idempotency record live in the same database, design the transaction so that the business change and its durable outcome record commit together. Otherwise, a crash after order creation but before recording completion can reopen the duplicate window.

For longer operations, distinguish an active attempt from a completed one. A concurrent retry needs a defined response or waiting policy. An “in progress” record also needs a recovery strategy; a crashed worker must not leave the operation permanently inaccessible.

Do not stop at the local transaction

An order can trigger a payment, email, or fulfillment request outside the database. Rolling back local rows cannot undo a remote service’s successful action. Record durable work and give each external business action a stable identity that survives worker retries.

A transactional outbox can commit intended work alongside the order. Its worker may still deliver a message repeatedly, so the receiver or downstream provider needs appropriate duplicate handling too. Where a provider supports idempotency, check its contract rather than assuming your local key protects the entire chain.

Publish the retry contract

Tell clients how long keys remain valid, which outcomes are retained, what happens during an active attempt, and how mismatched parameters are rejected. Retention is part of the guarantee: after an old record expires, the server may no longer recognize its key.

Keep sensitive request and response data only as needed, with normal access controls. A result identifier may be sufficient where storing the entire response would create unnecessary exposure.

Test the failure cases that challenge the design:

  • Two simultaneous submissions with one key.
  • A lost response after a successful commit.
  • A crash around the commit or downstream call.
  • One key reused with changed parameters.
  • A new intentional order with a fresh key.

The target is a documented retry window in which repeated attempts preserve the intended business result. A header alone cannot deliver that behavior; the durable records, concurrency rule, and recovery path have to agree.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top