An Idempotency Key Promises One Execution, Not Eventual Success
The design detail that surprises people: Stripe caches the result of the first request under a key even when that result was a 500, and replays the failure to every retry. That is correct, and once you see why, the rest of the design follows, including where most homegrown implementations go wrong.

Key takeaways
- An idempotency key guarantees that an operation executes once, not that it eventually succeeds, which is why Stripe saves the status code and body of the first request under a key regardless of whether it succeeded or failed and replays that same result, including a 500, to every retry.
- The client generates the key, and it must identify the logical operation rather than the attempt, so generating a fresh key per retry defeats the entire mechanism.
- Stripe recommends a V4 UUID or another random string with enough entropy to make collisions implausible.
- The server compares the parameters of a repeated request against the original and errors if they differ, because a reused key with changed parameters is a client bug rather than a retry.
- Stripe prunes keys after at least 24 hours, and a key reused after pruning is treated as a new request, so idempotency is a bounded window rather than a permanent guarantee.
- HTTP already defines GET, PUT and DELETE as idempotent at the method level; POST is not, which is precisely why POST is the method that needs an explicit key.
Here is the design decision that tells you whether someone has actually thought about idempotency or is repeating a definition.
Stripe saves the status code and body of the first request made under a given idempotency key regardless of whether it succeeded or failed. If that first attempt returned a 500, every subsequent retry with the same key returns the same 500.[1]
That sounds broken the first time you read it. It is correct, and the reason it is correct is the thing worth understanding: an idempotency key is a promise about how many times the operation executes. It is not a promise that the operation works.
“The guarantee is exactly-once execution, not eventual success. Retrying past a cached failure would be the bug, not the fix.”
Once you hold that distinction, the rest of the design falls out, and so do the four places homegrown implementations usually go wrong.
Why This Exists at All
A client sends a request to charge a card. The connection drops before a response arrives. The client now knows nothing useful: the request may have never landed, may have landed and failed, or may have succeeded with the response lost on the way back.
Its two options are both bad. Retry, and risk charging twice. Do not retry, and risk losing an order that the customer believes they placed. There is no third option available from the client alone, which is the point: this cannot be solved on the client, and no amount of careful timeout handling fixes it.
The idempotency key moves the decision to the server, which is the only party that knows what actually happened.
Plain English
The Four Places It Goes Wrong
1. Generating a new key per attempt. The single most common error, and it silently removes all protection. The key identifies the logical operation: one attempt to charge one customer for one order. Generate it once, before the first send, and reuse it for every retry.[1] A fresh UUID per attempt tells the server that each retry is a brand new charge, which is precisely the outcome you were defending against.
This means the key usually needs to be generated further up your stack than feels natural, and to survive a process restart if the retry might.
2. Not fingerprinting the parameters. If a client reuses a key but changes the payload, that is a bug in the client, not a retry. Stripe compares incoming parameters against the original and errors when they differ.[1]Returning the cached response instead would silently discard the caller's new intent and make the bug invisible, possibly for months.
3. Treating concurrency as a lookup.The naive implementation reads the key, sees nothing, and proceeds. Two retries arriving simultaneously both read nothing and both proceed, which reproduces the double execution exactly. You need a lock or a unique constraint that makes the second one wait or fail, not a read followed by a write. Stripe's own account notes that a request conflicting with another executing concurrently does not get a saved result.[1]
4. Storing keys forever. Idempotency is a bounded window, not a permanent ledger. Stripe prunes after at least 24 hours and treats a key reused after pruning as a new request.[1] Twenty four hours covers real network failures and client retries; keeping keys indefinitely grows a table without bound to defend against a retry nobody is going to send.
What to Store, Precisely
Not a boolean. Storing “this key was used” means a retry gets no useful answer, and the client is back where it started.
Store the response: status code and body.[1,2] A retry then receives byte-for-byte what the original caller would have received, which is what makes the retry genuinely transparent. The client does not need to know it was a replay.
And record it in the same transaction as the effect. If you charge the card and then crash before writing the key, the retry executes again and you have achieved nothing. If you write the key and then crash before charging, you have cached a success that never happened. This is the hard part of the implementation, and it is why the durable version lives in your database rather than in a cache, which is a different decision from ordinary caching despite looking similar.
Why this matters
Where It Is Heading
There is an IETF effort to standardise the header itself, so that Idempotency-Key means the same thing across APIs rather than being a per-vendor convention.[3] Worth tracking, and worth using the same header name now even while it is a draft, because the cost of matching the emerging convention is zero and the cost of inventing your own is a permanent integration note.
The Coupling Worth Naming
This is the other half of an argument I made about rate limiting: telling a client to retry after a 429 is only safe advice if retrying is safe. A rate limiter that returns Retry-After on a mutating endpoint with no idempotency key is instructing clients to do something that can duplicate their data.
The two features get built by different people at different times, and nobody owns the interaction. Which is a good general description of how this class of bug happens: not from either component being wrong, but from the seam between two correct components going unexamined. The same shape shows up across service boundaries, and it is why broken authorization, another seam problem, tops the OWASP API list.
Takeaway
An idempotency key guarantees one execution, not eventual success, which is why caching a 500 and replaying it is the correct behaviour rather than a bug. Generate the key once per logical operation and reuse it across retries; fingerprint the parameters so a changed payload errors instead of silently returning stale output; use a lock rather than a read-then-write so simultaneous retries cannot both proceed; store the full response in the same transaction as the effect; and prune after a day. Then never ship a Retry-After on a mutating endpoint that does not have one.
The same mechanism arrives from the other direction in messaging, where duplicates are not an edge case but the guarantee: exactly-once delivery does not exist, so the consumer has to be idempotent for the same reasons and with the same same-transaction requirement.
Sources and further reading
- 1.PrimaryStripe API reference, "Idempotent requests". Source for caching the first response including failures, V4 UUID key generation, parameter comparison, the 24-hour pruning window, and results being saved only after execution begins.
- 2.ReportingBrandur Leach, "Implementing Stripe-like idempotency keys in Postgres". The canonical engineering write-up on making the key record and the effect durable in one transaction.
- 3.ReportingHTTP Toolkit, "Working with the new Idempotency Keys RFC". On the IETF effort to standardise the Idempotency-Key header across APIs.
- 4.PrimaryStripe, "Designing robust and predictable APIs with idempotency". The original design rationale, including why the guarantee is about execution count rather than outcome.
Frequently asked questions
- What is an idempotency key?
- It is a unique value the client generates and sends with a request so the server can recognise later retries of that same request and avoid performing the operation twice. The server records the outcome against the key, and any repeat of the request returns the recorded outcome instead of executing again.
- Does an idempotency key mean my request will eventually succeed?
- No, and this is the most common misunderstanding. The guarantee is exactly-once execution, not success. Stripe saves the response of the first attempt whether it succeeded or failed, so if the original returned a 500, every retry with that key returns the same 500 rather than trying again.
- Should I generate a new idempotency key for each retry?
- No, that defeats the mechanism entirely. The key identifies the logical operation, such as one attempt to charge one customer for one order, so it must be generated once and reused across every retry of that operation. A fresh key per attempt tells the server each retry is a brand new request, which is how double charges happen.
- What happens if I reuse an idempotency key with different parameters?
- A well-built implementation rejects it. Stripe compares the incoming parameters against those of the original request under that key and errors if they do not match, because a changed payload under a reused key is a client bug rather than a retry, and silently returning the old result would hide it.
- How long do idempotency keys need to be stored?
- Long enough to cover realistic retry windows rather than forever. Stripe prunes keys after at least 24 hours and treats a key reused after pruning as a new request, which bounds the storage cost while still covering the network failures and client retries the mechanism exists for.
- Is an idempotency key the same as an idempotent HTTP method?
- No. HTTP defines GET, PUT and DELETE as idempotent by specification, meaning repeating them has the same effect as doing them once. POST carries no such guarantee, since it is defined as creating something new each time, and an idempotency key is how you add that guarantee to POST where the protocol does not provide it.
Written by
Tech Talk News Editorial
Computer engineering background. Writes about software, AI, markets, and real estate, and the places where the three meet.
More about the author