Journal / Backend, APIs, and System Design

Backend, APIs, and System Design

Idempotency Keys: A Pattern Every Senior Engineer Should Master

An idempotency key is an identifier the client generates and attaches to a write request, letting the server detect and deduplicate repeated calls to the same operation. The pattern converts an operation with side effects from one that is unsafe to retry into one that is safe to retry, which is the difference between a payment system that occasionally charges twice and one that never does.

What you need to know

  • Idempotency keys convert operations that are unsafe to retry into operations that are safe to retry. This is the design goal, and everything about the implementation should serve it.
  • The key must be generated before the first request, stored by the client, and reused for all retries. A key generated anew for each retry does not provide idempotency.
  • The implementation on the server requires a persistent store that maps keys to results, plus a TTL. Caching in memory is insufficient because server restarts lose the stored keys.
  • The partial execution edge case is the hardest part of the implementation. The server must handle the case where it received the key, began processing, failed partway through execution, and now receives the same key again.
  • Idempotency keys are implemented on the server but require cooperation from the client. Document the expected behavior clearly in the API reference.

The core argument

The pattern that most engineers implement first is the happy path: key comes in, check the store, if absent process and store, if present return the stored result. This works for the common case. The senior engineer distinction is in handling the edge cases: the request still in flight where the server started processing but has not yet stored the result, the race condition where two requests with the same key arrive simultaneously, and the failure where the operation was stored as successful but the response was lost in transit.

The case of a request still in flight requires a commit pattern with two phases: mark the key as processing before executing the operation, mark it as completed with the result after. If a request arrives for a key that is in the processing state, the server should either wait for the first request to complete or return a 409 Conflict that tells the client the operation is in progress. The race condition case requires a unique constraint at the database level on the key column, not just a check at the application level, to prevent simultaneous processing under concurrent requests.

The lost response case is often overlooked. The operation completed successfully and the result was stored against the key, but the response never reached the client. The client retries. The server returns the stored result. This is the intended behavior. The client must be designed to handle receiving the same response twice, which means the response body should contain enough information for the client to determine that the operation was already completed. Returning the resource ID and state in every mutation response makes this straightforward.

Common mistakes

  1. Using storage in memory for key results. Storage in memory is lost on server restart. A server restart during a period of high traffic means all idempotency keys still in flight become orphaned and retries execute as fresh operations.

  2. Not handling the partial execution case. An implementation that checks for keys but does not handle the case where execution started and failed will process the operation twice when the same key arrives after a failure partway through execution.

  3. Using timestamps as part of the key. A key based on a timestamp is not safe for retries that happen within the timestamp resolution. Use UUIDs.

  4. Setting the TTL based on convenience rather than client behavior. The TTL must be longer than the maximum realistic retry window. If clients retry for up to six hours after a network partition, a TTL of two hours is insufficient.

  5. Not including idempotency key support in the API changelog. Adding or changing idempotency key behavior is a breaking change for clients that have built around the previous behavior. Document it and version it appropriately.

Where to start

  1. Implement the database schema first. A table with key, status, result, created_at, and expires_at. Add a unique constraint on key. This schema serves the full pattern including the partial execution case.

  2. Write the middleware that handles the key check. Before the handler executes: check for existing key. If found and completed, return stored result. If found and processing, return 409. If absent, mark as processing and continue.

  3. Write the handler completion logic. After the handler executes successfully: update the key status to completed and store the response body. Run this in a transaction with the primary operation.

FAQ

Frequently asked

  • How should a client generate an idempotency key?
  • How long should the server store idempotency keys?
  • What should the server return on a duplicate key?
  • What happens if the first request fails before completing?
  • Do idempotency keys need to be globally unique, or unique only per endpoint?

Author

Closing note from the author

I keep these closing notes short on purpose. Most engineers writing about this topic are not the engineer you want to hire. I might be. Yashveer Singh, founder of Yashveer Labs. The contact channel is Instagram. The proof is the portfolio. The standard is in the work. If we are aligned, you will know within five minutes of the first message.

Start the conversation See the work DM on Instagram