Idempotent, Explained: Why It Matters for APIs and Retries
What idempotent means in practice — how it shapes HTTP method semantics, why it makes retries safe over flaky networks, and how the idempotency key pattern stops duplicate charges.
A request times out. You don’t know whether the server got it. Do you retry? Whether that’s safe comes down to one word: idempotent.
What idempotent actually means
An operation is idempotent if performing it many times produces the same result as performing it once. The state of the system after one call is identical to the state after ten identical calls.
The classic example is setting a value. x = 5 is idempotent — run it once or a hundred times and x is 5 either way. Compare that to x = x + 1, which lands somewhere different every time. The first is idempotent; the second is not.
Note what idempotent does not mean. It does not mean “no effect.” DELETE /users/42 changes the world the first time — the user is gone. But running it again leaves the system in the same state: the user is still gone, and the outcome is the same. The effect doesn’t repeat. That’s the distinction that matters.
This is also different from safe, a related term that gets confused with it. A safe operation has no side effects at all — it only reads. A GET request is safe: it shouldn’t change anything. Every safe operation is idempotent (reading twice changes nothing), but not every idempotent operation is safe. DELETE is idempotent but definitely not safe.
How HTTP methods encode this
HTTP method semantics are built on these two properties, and the spec defines which methods are which.
- GET — safe and idempotent. It reads a resource. Repeating it returns the same thing and changes nothing.
- PUT — idempotent, not safe. It replaces a resource with the body you send.
PUT /users/42with the same payload always leaves user 42 in the same final state, no matter how many times you send it. - DELETE — idempotent, not safe. Deleting an already-deleted resource leaves it deleted.
- POST — neither safe nor idempotent (in the general case).
POST /orderstypically creates a new resource each time. Send it twice and you get two orders.
That last line is the whole problem with retries. GET, PUT, and DELETE can be retried freely. POST cannot — a blind retry risks a duplicate.
The idempotency key pattern
Some writes genuinely create something new and can’t be made idempotent by their nature — charging a card, placing an order, sending a message. You still want to retry them safely, because networks fail in the worst way: the server processes your request, then the response gets lost on the way back. The client thinks it failed; the work is actually done.
The fix is an idempotency key. The client generates a unique identifier (often a UUID) for the logical operation and sends it with the request, usually in a header:
POST /charges
Idempotency-Key: 9f8c2a1e-7b3d-4e6a-9c12-0f5a8b2d4e10
{ "amount": 4200, "currency": "usd" }
The server records that key the first time it processes the request, along with the result. If a request arrives with a key it has already seen, the server skips the work and returns the stored result instead of doing it again. The client can now retry the same request as many times as it wants — same key, same outcome, exactly one charge.
This is how payment APIs handle retries; Stripe’s API, for instance, uses an Idempotency-Key header for exactly this reason. The pattern effectively makes a non-idempotent POST behave idempotently, scoped to that one key. A few details worth getting right: generate the key once per logical operation and reuse it across retries (a fresh key per attempt defeats the purpose), and have the server expire stored keys after some window so the table doesn’t grow forever.
FAQ
FAQ
Is POST ever idempotent?
What's the difference between safe and idempotent?
Why not just retry every failed request?
Related reading
2026-06-22
TCP vs UDP, Explained Through What Breaks When You Pick Wrong
TCP and UDP aren't interchangeable. We walk through the exact failure modes — head-of-line blocking, silent packet loss, Nagle delays — that show up when you pick the wrong transport.
2026-06-22
Write-Ahead Logging: How Databases Survive a Power Cut
How write-ahead logging keeps your data intact when the machine dies mid-write — the log-first rule, fsync, checkpoints, and why PostgreSQL and SQLite both rely on it.
2026-06-22
Backpressure, Explained Through a Queue That Won't Fall Over
What backpressure actually is, why an unbounded queue is a memory leak in disguise, and the four strategies a producer can take when a consumer falls behind.
2026-06-22
What a Bloom Filter Actually Saves You (and When It Lies)
A bloom filter trades a small false-positive rate for big memory savings. Here is the math behind the trade, where it pays off, and the failure mode that bites people.
2026-06-22
Idempotency, Explained Through the Retry That Doesn't Double-Charge
A practical look at idempotency keys: why a retried payment request shouldn't charge a card twice, how the pattern works, and where it quietly breaks in production.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.