What Is Idempotency? The Property That Makes Retries Safe

- Idempotent operations produce the same outcome no matter how many times they run: f(f(x)) = f(x)
- GET, PUT, and DELETE are idempotent HTTP methods; POST and PATCH are not, which matters the moment you add retry logic
- Idempotency keys convert non-idempotent operations (like payments) into safe-to-retry ones by caching the server's result after the first execution
- Upsert (
INSERT ... ON CONFLICT) is the idempotent database write; plain INSERT creates a new row every time - At-least-once delivery requires idempotent consumers: store a deduplication key before processing, check it on every attempt
- Pure functions are automatically idempotent; any function with external side effects needs deliberate design to be safe to retry
You click "Pay." Nothing happens. You click it again. Once more, just to be sure. The network finally catches up, and you have three pending charges on a $50 order.
That is not bad luck. That is what happens when someone did not think about what happens when a request runs twice.
Idempotency is the property of an operation that makes repeating it safe. An idempotent operation can run once or a hundred times and the outcome is identical. No accumulation. No drift. No apologetic email to customer support.
The math is f(f(x)) = f(x) for every valid input. The engineering consequence follows directly: distributed systems that depend on retries cannot function correctly without it.
The Elevator Button Test (You Have Already Failed This)
Pressing an elevator button is idempotent. Press 7 once and the elevator goes to floor 7. Press it twelve more times in frustration because the door is taking forever. Still floor 7. The call is registered; subsequent presses change nothing.
Adding items to a shopping cart is not idempotent. Click "Add to Cart" three times and you have three items. Every press mutates state. The world has changed.
One is safe to retry. The other is not.
That distinction determines how you design every API, database write, and event consumer you will ever ship. Get it wrong and users double-pay, confirmation emails send three times, and records duplicate themselves in production at 2 a.m. while you are very much asleep.
When the server returns nothing and you have no idea if clicking again is a catastrophic mistake or totally fine.
What Passes the Test and What Doesn't
Operations that pass:
Math.abs(-5)returns 5. Run it a thousand times. Still 5. Profoundly unexciting.UPDATE users SET active = true WHERE id = 123. Run it ten times. Still true.- Adding an element to a set.
{1, 2, 3}.add(2)is still{1, 2, 3}. Sets do not care about your enthusiasm. DELETE /users/123. After the first call the user is gone. Subsequent calls find nothing to delete. End state: identical.
Operations that fail:
counter += 1. Grows every call. Runs twice, charges twice, somebody screams.INSERT INTO orders VALUES (...). New row every time.POST /orders. New order on every request. Retry on network timeout, enjoy your duplicate.
Operations that set state to a specific value are idempotent. Operations that transform state are not. That is the whole pattern. Everything else is an application of it.
How HTTP Methods Split on Idempotency
REST divides its methods along these lines, and this table comes up in nearly every backend system design interview.
| Method | Idempotent | Safe (no side effects) |
|---|---|---|
| GET | Yes | Yes |
| HEAD | Yes | Yes |
| PUT | Yes | No |
| DELETE | Yes | No |
| POST | No | No |
| PATCH | Usually not | No |
GET is both safe and idempotent: reading a resource does not change it, and reading it repeatedly changes nothing. Browsers cache GET responses freely for exactly this reason.
PUT is idempotent because it replaces a resource entirely. PUT /users/123 {"name": "Alice"} sets the name to Alice regardless of what it was before. Call it five times; the user's name is still Alice. No harm done.
POST creates resources. POST /orders makes an order. Call it five times and you have five orders. This is why browsers throw up that "Resubmit this form?" warning when you hit refresh on a checkout page. They are trying, earnestly, to save you from yourself.
DELETE is the interesting case. The first call removes the resource. The second finds nothing. The status code might differ (204 vs. 404) but the end state is the same: the resource does not exist. Idempotent by outcome, even when the response code changes.
PATCH is "usually not" because it typically sends a diff, not a full replacement. PATCH /account {"balance": {"increment": 10}} is not idempotent. PATCH /account {"balance": 150} is. The method does not determine idempotency. The payload semantics do.
With an idempotent operation, you do not need to know whether a failed request was processed before it failed. Retry freely. With a non-idempotent operation, a blind retry on timeout is how users end up on hold with customer support.
One UUID That Saves Your Users' Wallets
When you must make a non-idempotent operation safe to retry, the standard solution is an idempotency key. The client generates a unique ID for the request and sends it with every attempt.
POST /payments Idempotency-Key: a47e3c9d-5c2a-4b1f-a2c6-8e93bb1c2f7a { "amount": 50, "currency": "USD" }
The server stores the key and the result on first execution. On subsequent requests with the same key, it returns the stored result without reprocessing. The card is charged once regardless of how many times the network hiccups.
def process_payment(idempotency_key: str, amount: int) -> dict: cached = cache.get(idempotency_key) if cached: return cached result = charge_card(amount) cache.set(idempotency_key, result, ttl=86400) return result
The TTL matters here. 24 hours is a common window, long enough to cover network flakiness and exponential backoff retry loops, short enough that you are not storing every key forever. Go shorter and a slow retry can slip past the cache. Go much longer and you are paying for storage of keys that will never be used again.
Stripe, PayPal, and every serious payment processor exposes this interface. It converts a non-idempotent operation into an idempotent one by tracking what has already been done. If you are building a payment flow and not using this pattern, please fix that before you ship.
A non-idempotent payment endpoint with retry logic, unmasked.

Upsert Is the Idempotent Database Write
The database equivalent of the idempotency key pattern is the upsert: insert if the row does not exist, update if it does.
-- Non-idempotent: creates a new row every time INSERT INTO users (email, name) VALUES ('[email protected]', 'Alice'); -- Idempotent: same end state no matter how many times you run it INSERT INTO users (email, name) VALUES ('[email protected]', 'Alice') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;
Database migrations use this constantly. A migration that creates a table should check for its existence first. Run it twice on the same database and nothing explodes. This is the kind of thing you do not appreciate until you have triggered a migration twice on a production database and watched it try to create a table that already has six months of data in it.
In event-driven systems the upsert is your primary tool. When a Kafka consumer receives an "order created" event, it can upsert the order record by order ID. Process the event ten times and the result is one order. The deduplication key is the primary key of whatever you are writing.
Distributed Systems Make This Non-Optional
In a distributed system, exactly-once message delivery is famously hard, often impossible at scale. The realistic alternatives are at-most-once (the message might drop) and at-least-once (the message will arrive, but possibly multiple times).
At-least-once delivery is the practical default, which means your consumers must be idempotent. If a payment service receives the same event twice from Kafka or SQS, it needs to detect and discard the duplicate rather than charging the customer again. You handle this with a deduplication key stored in the database: check for it before processing, store it after.
The message queue and pub/sub comparison covers delivery semantics in detail. The short version: design for at-least-once, and make every consumer idempotent through deduplication keys or upsert writes.
Idempotency failures and race conditions look identical from the outside. Both produce double charges, duplicate records, and confused users. The root causes differ: race conditions come from concurrent execution; idempotency failures come from retries. Both require deliberate design to prevent, and distinguishing between them when debugging is half the work.
Where This Shows Up in Interviews
You will run into idempotency in two places.
System design. Any payment system, order system, or event pipeline design should address retry behavior. "What happens if this message is delivered twice?" is a standard follow-up. The answer involves idempotency keys or deduplication at the consumer. The system design idempotency guide goes deep on patterns for specific designs.
DSA and pure functions. A memoized function is idempotent: call it with the same input once or fifty times, the result and cache state are identical after the first call. See the memoization explainer for how that plays out. A function that appends to a global list as a side effect is not idempotent: call it twice and the list grows.
The easiest path to idempotency is pure functions. A function that takes input, returns output, and produces no side effects is always idempotent for the same input. If your function modifies state outside its scope, you need to reason carefully about what happens when it runs twice.
One Question to Ask Before You Ship
Before you deploy any API endpoint, database migration, or message consumer, ask: "What happens if this runs twice?"
If the answer is "the same thing as running it once," you are fine.
If the answer involves "another row," "another charge," or "another email," you have work to do before you push that button.
Idempotency is not a property you bolt on after the system is running. It is a decision you make when you design the operation. The retry scenario is always there. The only question is whether you thought about it first, or whether you find out at 2 a.m. from a user who got charged three times.
If you want to practice answering "what happens if this message is delivered twice?" under real interview pressure, SpaceComplexity runs voice-based mock interviews where system design rounds surface this question exactly as a real interviewer would.