Failure Modes Retried Request → Idempotency Key

Retried requests.
One record.

A request can arrive twice: timeouts, double clicks, client retries. Retry one: the naive API records the request again; with an idempotency key the retry replays the first response.

charged $0 · 0 sent · 0 retried

Naive, charges every request

customer charged
$0
overcharged
$0
client
sends
payment api
charges
postgres
stores
payments

Idempotency key, one charge per key

customer charged
$0
replayed
$0
client
sends
payment api
checks key
postgres
stores
idempotency_keys
payments
replayed
no retries yet

Send a request, then retry it: networks time out, users double-click, clients resend.

How idempotency keys actually work read more

If a request times out, the client can't tell whether it worked. Maybe only the response got lost on the way back. Retrying is the only safe option. But a naive API treats that retry as a brand new request, so the payment goes through twice.

The fix: the client makes up one key per payment and sends it with every attempt, including retries. The API saves that key, together with the response it's about to send back, in the same transaction as the payment itself:

BEGIN;
  INSERT INTO idempotency_keys (key, response) VALUES ($1, $2);
  -- unique(key): a retry hits this and gets the stored response back
  INSERT INTO payments (..., status='paid');
COMMIT;

A retry sends the same key, the unique constraint blocks the second insert, and the API just hands back the response it already saved, so a replay looks identical to the original request. The order matters: if the API checked for the key first and inserted it second, two retries arriving at the same moment could both pass the check and both go through.

The trade-off: the client has to create that key and hold onto it even if it crashes, the API has to store every response, and someone has to decide how long a key stays valid. None of that is a default. It's a decision.