Lesson 9 of 17
24. APIs and Idempotency

24.8 How do idempotency keys work?

3 min read+10 XP

At the machine: Each purchase gets a receipt number the moment you start. If you tap again with the same receipt number, the machine says "already paid, here's your water", and doesn't charge you again.

In the system: The client generates a unique idempotency key for each intended operation (a UUID, or something like checkout-881) and sends it with the request, usually in an Idempotency-Key header. It sends the same key on every retry of that operation. The server:

new key: claim and process; seen and finished: replay the stored result; still running: 409; different body: 422.

Figure: the server remembers each key and what it answered.

  1. New key: atomically claim it (insert a row with status "in progress"; a unique constraint makes sure only one request wins), do the work, then store the response with the key.
  2. Key seen, finished: return the stored response. Don't do the work again.
  3. Key seen, still in progress (a concurrent retry): return 409 and let the client retry shortly.
  4. Key seen with a different request body: return an error (422), because the client reused a key by mistake.
  5. Expire keys after a while.

This is how Stripe's API works: it saves the status code and body of the first request for each key (even a 500 error) and returns them for later requests with the same key; keys can be removed once they're at least 24 hours old; and reusing a key with different parameters is rejected. There is also an IETF draft standardising the Idempotency-Key header.

In the chapter's code: the same key twice gives one charge and the same response; the same key with a different amount gives 422; a request while the first is still running gives 409; and the same key after 24 hours is treated as new.

Details that matter:

  • Who makes the key: the client, once per intended action, stored with the order so it survives a client restart.
  • The claim must be atomic. "Check if the key exists, then charge, then save" lets two racing retries both charge (the Level 4 bug).
  • Store the key in the same database transaction as the effect where you can, so they can't disagree.
  • Business keys work too: "one payment per order 881" as a unique constraint is an idempotency key in disguise (Chapter 17).

Memory code: "Same receipt number, same result, no second charge."