API Design: Idempotency Keys for Safe Retries

2026-05-02

Network requests fail. Clients retry. And suddenly your user gets charged twice, or two identical orders are created. The solution is idempotency — ensuring that performing the same operation multiple times produces the same result as performing it once.

GET, PUT, and DELETE are naturally idempotent by definition. The problem child is POST. Creating a resource, processing a payment, or sending a notification — these operations have side effects that shouldn't be duplicated. This is where idempotency keys come in.

The pattern is straightforward: the client generates a unique key (typically a UUID) and sends it in a header with the request:

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

The server then follows this logic:

The implementation requires care in a few areas:

Storage and expiry. Store idempotency keys with a TTL. Stripe uses a 24-hour window — long enough for retry storms, short enough to not bloat storage forever. A reasonable rule of thumb: set your TTL to 10x your maximum expected retry window. If clients retry for up to 1 hour, expire keys after 10 hours.

Race conditions. Two identical requests can arrive simultaneously. Use an atomic operation — an INSERT with a unique constraint, or a Redis SETNX — to claim the key. The loser should either wait for the winner to finish or return a 409 Conflict.

What to store. Store the full response: status code, headers, and body. The client should get back exactly what it would have received on the first call, including error responses. If the first attempt returned a 400 validation error, the retry should get the same 400 — not re-process and potentially succeed with changed server-side state.

Scope the key correctly. Bind idempotency keys to the authenticated user or API key. Otherwise, one client's key could collide with another's. The composite key is typically (client_id, idempotency_key).

A real-world example: a payment service receives a charge request. The client crashes before reading the response. On retry with the same idempotency key, the server finds the stored result showing the charge succeeded and returns it. The customer is charged exactly once. Without this, you'd need manual reconciliation — or angry customers.

One common mistake: making the key optional. If your endpoint has dangerous side effects, require the idempotency key on POST requests. Returning a 400 when it's missing is better than processing duplicate payments.

See it in action: Check out Idempotency - What it is and How to Implement it by Alex Hyett to see this theory applied.
Key Takeaway: Require client-generated idempotency keys on non-idempotent endpoints, store the full response atomically, and expire keys after 10x your maximum retry window to make retries safe by default.

All newsletters