Back to blog
Ai News

Idempotency Api Design

5 min read

Every API that does something consequential — charge a card, create an order, send a message — eventually has to answer an uncomfortable question: what happens when a client sends the same request twice? Not because the client is buggy, necessarily, but because networks are unreliable. A request times out after the server already processed it, the client retries, and now you've charged someone twice for one purchase. Idempotency keys are the standard answer to this, and the pattern — largely popularized by Stripe's API design — has become close to a default expectation for any API that handles anything with real-world consequences.

What idempotency actually means here

In the mathematical sense, an idempotent operation is one where doing it once and doing it five times produce the same result. PUT and DELETE are naturally idempotent in REST — setting a resource's state or deleting it produces the same end state no matter how many times you do it. POST, though, typically isn't — creating a new order or charging a card is an action with a side effect, and calling it twice naturally creates two orders or two charges.

An idempotency key retrofits that "same result no matter how many times" property onto a naturally non-idempotent operation. The client generates a unique identifier, attaches it to the request, and the server uses it to recognize "I've already handled this exact request" on any retry — returning the original result instead of processing it again.

The core pattern

The implementation details are fairly consistent across the APIs that do this well:

The client generates the key, not the server. This is a common early mistake — if the server generates the idempotency key, it can't help with the actual failure case, which is the client not knowing whether its original request succeeded. The client needs to generate the key before sending the request, so it can safely retry with the same key if the response never arrives.

Use a high-entropy identifier. A UUIDv4 or a random string with at least 128 bits of entropy is the standard recommendation — enough that two legitimately different requests will essentially never collide on the same key by chance.

Pass it in a dedicated header, typically Idempotency-Key, rather than burying it in the request body. This convention, originating from Stripe's API, has become widely enough adopted that many API consumers now expect to find it there by default.

Store the key with the original response, not just a flag that "this happened." When a retry comes in with a matching key, the server should return the exact original response (same status code, same body) rather than a generic "already processed" message — because the client's retry logic is often just replaying the same request-handling code and expects a normal-shaped response.

Set a retention window and document it. Idempotency keys can't be stored forever — most APIs retain them for somewhere between 24 hours and a few days, long enough to cover realistic retry scenarios but not forever. Whatever the window is, it needs to be documented, because a client that assumes an indefinite window and one that assumes a short one will behave differently in edge cases.

The part that trips people up: parameter mismatches

Here's a scenario that's easy to miss in a first implementation: what happens when a client sends the same idempotency key twice, but with different request parameters? This isn't necessarily malicious — it might be a client bug, or two genuinely different logical requests that accidentally reused a key. The safe behavior is to reject the second request with a clear error rather than either silently processing it as a new request (which defeats the purpose) or silently returning the first response (which could return the wrong data for a genuinely different intended action). Skipping this check is one of the more common gaps in home-grown idempotency implementations — it's easy to build the happy path and forget the mismatch case entirely.

Concurrency: the second common gap

The other subtle failure mode is concurrent requests using the same key arriving close together — a client that retries aggressively, or a race between a legitimate request and an accidental duplicate fired at nearly the same moment. Without a lock, both requests can pass the "has this key been seen before" check simultaneously and both proceed to execute the underlying operation. The standard fix is a distributed lock (or a database-level unique constraint on the key, with the second insert failing fast) so that only one request actually executes while the other waits for, and then returns, the first one's result.

Why this matters beyond payments

Idempotency keys get discussed most often in the payments context because a duplicate charge is the most viscerally bad outcome, but the same pattern matters anywhere a retry could create a duplicate side effect: sending a notification or email, creating a support ticket, submitting an order, triggering a webhook. Any API endpoint where "processed twice" is meaningfully worse than "processed once, confirmed" is a candidate for this pattern — which in practice covers a large share of the write endpoints in most production APIs.

A pragmatic implementation checklist

  1. Accept the key via a dedicated header, generated client-side, with enough entropy to avoid accidental collisions.
  2. Store the key alongside the full response (status + body) the first time it's processed.
  3. On a matching key, return the stored response rather than reprocessing.
  4. Reject a matching key sent with different request parameters, rather than silently accepting either interpretation.
  5. Use a lock or unique constraint to prevent two concurrent requests with the same key from both executing.
  6. Document your retention window explicitly — don't leave clients guessing how long a key stays valid.
  7. Make it optional but strongly encouraged for write endpoints with real side effects — clients that don't send a key should still work, just without the retry-safety guarantee.

Idempotency isn't a feature you bolt on after something goes wrong in production — it's a design decision that's much cheaper to build in from the start than to retrofit once clients are already relying on the endpoint's current (non-idempotent) behavior. For any API doing payments, order creation, or anything else where a duplicate is a real problem, treating idempotency as a first-class part of the endpoint contract, not an afterthought, tends to save a very bad on-call incident later.

Sources: Stripe: Designing robust and predictable APIs with idempotency, Zuplo: Implementing Idempotency Keys in REST APIs, Aleksei Aleinikov: Idempotency Keys Explained 2026

Get new posts as they publish

No spam — just the next post, straight to your inbox.

Keep reading

Discussion