You click “Pay”, the page spins, and then it times out. You have no idea whether the payment went through. Do you click again and risk being charged twice, or wait and risk not paying at all? That exact moment of dread is the problem idempotency solves. It is one of those words that sounds academic and turns out to describe something you feel every time an app hangs. Once it clicks, you see it everywhere, and you understand why serious backends are built around it.
Press the button five times, still one elevator
Idempotency means an operation can be repeated any number of times and the end result is the same as doing it once. The everyday picture is an elevator button. You press “floor 3” once, or you jab it five times because you are impatient; the elevator still arrives at floor 3, once. Pressing again changes nothing. That button is idempotent.
Now picture a button that instead toggles: press once and the light turns on, press again and it turns off. Repeating that is not safe, because the result depends on how many times you pressed. That is what a naive “charge this card” endpoint is like, and it is exactly what you do not want when the network is deciding how many times your request gets sent.
Why the network makes this unavoidable
Here is the part beginners miss: the danger is not the user clicking twice. It is that the network loses responses. Your request can reach the server, the server can do the work, and the reply can vanish on the way back. From the client’s side, a success that lost its response and a total failure look identical. So the client does the sensible thing and retries, and now the same action is on its way to the server a second time.
flowchart LR C[Client sends charge] --> S[Server charges the card] S -->|response lost on the way back| X[Client sees a timeout] X -->|assumes it failed, retries| S2[Server charges AGAIN] S2 --> D[Customer billed twice]
Retries are not an edge case you can avoid. Mobile networks drop, load balancers time out, message queues are built to deliver “at least once” on purpose. Retries are everywhere, so every action that must not happen twice has to be safe to repeat. That is the whole reason this concept exists.
The methods that are already safe, and the two that are not
If you build web APIs, some of this is handled for you, because HTTP already labels its methods. An idempotent method can be repeated without changing the result beyond the first time.
| Method | Idempotent? | What repeating it does |
|---|---|---|
| GET | Yes | Just reads; changes nothing |
| PUT | Yes | Sets the resource to a value; setting it again lands the same |
| DELETE | Yes | Gone after the first; later calls just find it already gone |
| POST | No | Each call creates a new thing, so repeating makes duplicates |
| PATCH | No | Depends on the change; “add 10” repeated keeps adding |
The lesson in the table: PUT (“set the name to Asad”) is safe to repeat, POST (“create a new order”) is not. So a design instinct worth having is to prefer setting a known state over triggering an action, where you can. But payments are POST by nature, so for those you need one more tool.
The idempotency key: making POST safe too
For the actions that cannot be idempotent on their own, the standard fix is an idempotency key.
The client generates a unique value once (a random UUID), before the first attempt, and sends it with
the request in a header, usually Idempotency-Key. Every retry of that same logical request carries
the same key. This is exactly how Stripe’s payment API works.
The server uses the key as a memory of what it has already done:
flowchart TD
R[Request arrives with an idempotency key] --> Q{Seen this key before?}
Q -->|no| P[Do the work, e.g. charge the card]
P --> Sv[Store key plus the response]
Sv --> Ret[Return the response]
Q -->|yes| Cached[Skip the work, return the stored response]
So the first request charges the card and records the outcome under that key. The retry arrives with the same key, the server sees it, and instead of charging again it hands back the stored result. The customer is charged once, and the client still gets a clean answer. The stored result is returned for every repeat, even if the first attempt was an error, because the point is that the same key always gives the same answer.
The one detail that makes it actually safe
This is where a working demo and a correct system part ways, and it is worth understanding even as a beginner. It is not enough to “check if we have seen the key, and if not, do the work.” Two requests with the same key can arrive at nearly the same instant, both check, both see nothing, and both charge. The check-then-act has a gap.
Two things close it. A unique constraint on the key column in the database, so the database itself refuses a second row with the same key, and doing the recording and the effect in the same transaction, so they either both happen or neither does.
await db.transaction(async (tx) => {
// the DB has a UNIQUE index on idempotency_key.
// if this key already exists, this insert throws, and we return the stored result instead.
await tx.insertKey(key) // claims the key
const result = await charge(amount) // does the real work
await tx.saveResult(key, result) // records the outcome
return result
}) // all three commit as one, or roll back as one
IMPORTANT
The database is what makes idempotency real, not the application code. A unique constraint plus one transaction turns “we probably will not double charge” into “we cannot”. Never rely on an in-memory check or a “look before you leap” that lives outside the transaction; that is the gap two simultaneous retries slip through.
TIP
The client generates the key once, before the first try, and reuses it for every retry of that same action. A fresh key per attempt defeats the whole thing, because to the server each attempt looks like a brand new request.
Where you will meet this
You do not have to be building a bank. The moment your app does anything that must happen exactly once, this shows up: charging a card, placing an order, sending an email, processing a webhook a provider will re-deliver, or consuming a message off a queue that guarantees “at least once”. Any of those, retried, is a bug unless the action is idempotent. It is a quiet backbone of reliable systems, and once you know the word you notice how much of the internet leans on it.
Idempotency is the fundamental behind a boring, wonderful promise: it is safe to try again. In a world where the network will drop your response and something will always retry, “safe to repeat” is not a nicety. It is the difference between a system you can trust and one that charges your customer twice at the worst possible moment.
Build it so trying again is always safe.
Sources
- Stripe: designing robust and predictable APIs with idempotency for the idempotency key, storing the first response, and returning it on every retry
- Stripe API reference: idempotent requests for the
Idempotency-Keyheader and using a V4 UUID - brandur.org: implementing Stripe-like idempotency keys in Postgres for the unique constraint and recording the key in the same transaction as the effect
- MDN: idempotent and restfulapi.net: idempotent REST APIs for which HTTP methods are idempotent and why