What Is Idempotency in APIs? A Practical Guide
Photo by Unsplash on Unsplash
Table of Contents
The double charge problem
Here's the scenario every payments engineer has lived through.
A user taps Pay. Your server calls the payment provider. The charge succeeds. Then the response times out on the way back, or the user's train goes into a tunnel, or your load balancer decides that connection has had enough.
Your client sees a failure. It retries, because retrying failed requests is sensible and every HTTP library does it. The provider receives a second, identical charge request and, having no reason to think otherwise, charges the card again.
The user is now down twice the money and you have a support ticket, a refund, and a small dent in your reputation. I've been on the wrong end of this as a customer too, which is a memorable way to learn why it matters. Nothing in that chain was a bug exactly. Every component did the reasonable thing. The system was just missing a property.
That property is idempotency, and once you see it you start noticing where it's missing everywhere.
The definition, without the maths
An operation is idempotent if performing it many times leaves the system in the same state as performing it once.
Note what that does and does not say. It says nothing about the *response* being identical. It says nothing about side effects like logs or metrics. It's about the resulting state.
Deleting a file is idempotent. Delete it once and it's gone. Delete it four more times and it's still gone, no more and no less deleted. The second call might return a different status code, and that's fine.
Setting a value is idempotent. `balance = 100` run five times leaves the balance at 100.
Adding is not. `balance = balance + 100` run five times leaves you five hundred richer than intended. That's the whole distinction, and it's why "create an order" and "increment a counter" are the dangerous shapes while "replace this record" is the safe one.
A useful habit when designing an endpoint: ask whether it *sets* or *adds*. Setting operations tend to be naturally idempotent. Adding operations need protection. MDN's glossary entry on idempotent is a good short reference if you want the formal HTTP framing alongside this.
Which HTTP methods are safe by spec
GET is idempotent and also safe, meaning it shouldn't change state at all. Fetching a resource ten times gives you the resource ten times.
PUT is idempotent. It replaces a resource with the body you sent. Send it repeatedly and the resource ends up in that state regardless.
DELETE is idempotent. Gone stays gone.
HEAD and OPTIONS are idempotent for the same reasons as GET.
POST is not idempotent, by design. It means "process this", and processing it twice is expected to do the thing twice. That's why creating resources uses POST.
PATCH is the interesting one. It is not idempotent in general, because a patch can express a relative change. A patch that says "set status to shipped" happens to be idempotent. A patch that says "add 5 to quantity" is not. The method doesn't guarantee it, so you can't rely on it.
Why does the spec table matter in practice? Because proxies, browsers, service meshes and HTTP client libraries all read it. A well behaved client may automatically retry an idempotent request after a network error, and will typically refuse to auto-retry a POST. If you build a POST endpoint that quietly assumes retries never happen, you've built on an assumption the spec explicitly does not make for you. The method rundown in the HTTP methods explainer covers the rest of the semantics if you want the fuller picture.
How an idempotency key actually works
The client generates a unique identifier before it sends the request and puts it in a header:
``` POST /v1/charges Idempotency-Key: 8f14e45f-ea0f-4d3a-9c1b-2b6ac1e8b6a1 ```
The critical detail, and the one people get wrong: the key is generated once per logical operation, not once per attempt. If the request fails and you retry, you send the *same* key. Generating a fresh key on retry defeats the entire mechanism, and I've reviewed production code that did exactly that.
Server side, the flow is roughly:
1. Read the key. If there's no key on an endpoint that requires one, reject with a 400. 2. Look it up in a store. When it's absent, claim it atomically and mark it in progress. 3. Newly claimed keys go on to do the real work, then save the response body and status against the key. 4. Already complete? Return the stored response without repeating any of the work. 5. Still in progress means a 409, so the client backs off rather than racing.
Step 2 has to be atomic or you've just moved the race condition. Use a unique constraint on the key column, or a Redis `SET NX`. Doing a read-then-write in application code will fail exactly when two retries arrive together, which is precisely the situation you built this for.
For the key itself, a UUID is the obvious choice, and the UUID generator is handy when you're poking at an endpoint by hand rather than through a client library. I keep one open in a tab whenever I'm testing this sort of endpoint.
Storage, expiry, and the bits people skip
Keys need an expiry. Storing them forever means an ever growing table, and nobody retries a request from three months ago. Twenty four hours is the common window and it's a reasonable default. Stripe's published behaviour has long been in that neighbourhood, and it's long enough to cover any realistic retry storm.
You need to store the response, not just the fact that the key was used. Returning a bare 200 with no body to a retry breaks clients that expected the charge object. Store the status code and the serialised body.
Scope the key correctly. It should be unique per account or per API key, not globally. Two different customers generating the same UUID is vanishingly unlikely, but scoping also stops one tenant probing another's keys, which is a real if unglamorous security consideration.
Decide what happens when the same key arrives with a *different* body. Somebody's client has a bug. The safest behaviour is to reject with a 422 and an explicit error, rather than silently returning the old response for a request that asked for something else. Hashing the request body and storing the hash alongside the key makes this check cheap.
And be honest about what idempotency does not give you. It makes retries safe. It does not make your operation atomic, it does not fix a partial write halfway through a multi step process, and it does not help if the work happens in a background job that fails after you've already recorded the key as complete. Those need transactions and outbox patterns, which are a separate conversation from this one. If the broader request and response model is still fuzzy, the what is an API walkthrough is the right place to start before layering this on top.
Frequently Asked Questions
What does idempotent mean in simple terms?
An operation is idempotent if running it repeatedly leaves the system in the same state as running it once. Deleting a file is idempotent because it stays deleted, and setting a value to 100 is idempotent because it stays 100. Adding 100 to a balance is not, because each run changes the result.
Is POST idempotent?
No. POST means process this request, and processing it twice is expected to do the work twice, which is why it's used for creating resources. If a POST endpoint must not run twice, you add idempotency keys on top, since the method itself gives you no protection.
Is PATCH idempotent?
Not guaranteed. It depends entirely on what the patch expresses. Setting status to shipped is idempotent, while incrementing a quantity by five is not. Because the method makes no promise either way, you can't rely on infrastructure or clients treating PATCH as retry safe.
What is an idempotency key?
A unique identifier the client generates for one logical operation and sends in a header, usually Idempotency-Key. The server stores it with the resulting response, so a retry carrying the same key gets the original response back instead of triggering the work again. A UUID is the usual choice, and the [UUID generator](/tools/uuid-generator) works for testing by hand.
Should I generate a new idempotency key when retrying?
No, and this is the most common implementation mistake. The key identifies the logical operation, not the individual attempt. Generate it once before the first send and reuse the exact same key for every retry of that operation, otherwise the server sees each attempt as a brand new request.
How long should idempotency keys be stored?
Around twenty four hours is the common default and covers any realistic retry window. Storing them permanently grows the table forever for no benefit. Make sure you store the response body and status alongside what decideso, not just the fact it was used, or retries get an empty response the client can't handle.
Try ToolsFuel
23+ free online tools for developers, designers, and everyone. No signup required.
Browse All Tools