← All posts

Idempotency keys, or how to make retries safe

The network failed after the charge but before the response — now what

Key takeaways
  • A timeout tells you nothing about whether the work happened.
  • Idempotency is a property of the endpoint, not of the client.
  • Store the key with the result, not just the key.

A client calls POST /payments. The charge succeeds. The response times out on the way back.

The client has no idea whether it worked. If it retries, you might charge twice. If it does not, the user might not be charged at all. Neither is acceptable.

The shape of the fix

The client generates a unique key per intent — not per attempt — and sends it:

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

The server, on receiving a request:

  1. Look up the key. If it exists with a stored response, return that response and do nothing else.
  2. If it does not exist, insert it inside the same transaction as the work.
  3. Store the response body against the key when you finish.

The part people miss

Storing only the key is not enough. If you record "this key was seen" but not what you returned, a retry gets a different answer than the original call — often a 409 for something that actually succeeded. The client still cannot tell what happened.

Store the response. Replay it verbatim.

Which methods need it

GET, PUT and DELETE are already idempotent by definition. POST is not, which is exactly why it needs the key.

Expiry

Keys cannot live forever. Twenty-four hours is typical — long enough to cover any sane retry window, short enough that the table does not grow without bound.