Idempotency
An operation is idempotent if doing it once or many times gives the same result.
1Set balance = 500 → run 3 times → balance = 500 ✔ idempotent2Add 500 to balance → run 3 times → balance + 1500 ✘ not idempotentThink of a lift button: pressing it 5 times doesn't call 5 lifts. One press or many, the result is the same.
Why It Matters
Networks fail. A client often can't tell if a request failed or just the response got lost.
Ravi gets charged twice. Retries are necessary for reliability, so the server must make them safe. That's what idempotency does.
HTTP Methods
| Method | Idempotent? | Why |
|---|---|---|
| GET | ✔ | Only reads |
| PUT | ✔ | Replaces with the same value every time |
| DELETE | ✔ | Once deleted, it stays deleted |
| POST | ✘ | Usually creates something new each time |
| PATCH | Not guaranteed | "set name=Ravi" is, "increment count" isn't |
Idempotency is about the server's state, not the response. A second DELETE may return 404, but the state is still the same.
Safe vs idempotent: safe means "changes nothing" (GET). Idempotent means "repeating changes nothing extra" (PUT, DELETE).
Idempotency Key (The Main Technique)
The client generates a unique ID per operation and sends it with the request. The server remembers keys it has already processed.
1POST /payments2Idempotency-Key: 7f3c-91ab-...3{ "amount": 500 }1Server receives request with key 7f3c2 → Key seen before?3 YES → return the saved response (don't charge again)4 NO → process payment → save (key → response) → return responseSame retry, same key, one charge. This is how Stripe and most payment APIs work.
Important Details
| Case | What to do |
|---|---|
| Same key, request still processing | Return 409 Conflict or make the client wait; don't run it twice |
| Same key, different body | Reject with 422: the key was reused by mistake |
| How long to keep keys | Use a TTL, e.g. 24 hours |
| Where to store | Redis (fast) or DB table with a unique constraint on the key |
| Key generated by | The client (a UUID), created once per operation, reused on every retry |
Race Condition
What if two retries arrive at the exact same moment?
1Time Request A (key 7f3c) Request B (key 7f3c)2t1 Is key 7f3c saved? → No3t2 Is key 7f3c saved? → No4t3 Charge ₹500 ✔5t4 Charge ₹500 ✔ ← double charge ✘Both checked before either one saved the key, so both thought they were first.
Think of two people trying to book the last seat in a cinema at the same time. If the system only "looks" at the seat first and "books" it later, both may see it as empty. The fix is to look and book in one single step.
That single, uninterruptible step is called an atomic operation. Two common ways:
| Where | How | Result |
|---|---|---|
| Redis | SET 7f3c "IN_PROGRESS" NX | NX = "set only if it doesn't exist". First request succeeds, second gets "already exists" |
| Database | INSERT key with a unique constraint | First insert succeeds, second fails with a duplicate error |
1Time Request A Request B2t1 Claim key 7f3c → success ✔3t2 Claim key 7f3c → already taken ✘4t3 Charge ₹500 ✔ Return 409, or the saved resultNow only one request does the work.
Crash Safety
Another problem: the server crashes between two steps.
1Case 1: Charge ₹500 ✔ → crash → key never saved2 Retry sees no key → charges again ✘3 4Case 2: Save key ✔ → crash → payment never done5 Retry sees key → skips → Ravi never paid ✘Fix: save the key and do the work in the same database transaction. A transaction is all-or-nothing: either both happen, or neither does.
1BEGIN2 insert idempotency key 7f3c3 create payment record ₹5004COMMIT → both saved together, or nothing saved on crashIf the work calls an outside system (like a payment gateway) that can't join your transaction, pass the same idempotency key to that system too, so it also ignores duplicates.
Other Ways to Get Idempotency
| Technique | Example |
|---|---|
| Unique constraint | One order per (userId, cartId) |
| Upsert | INSERT ... ON CONFLICT DO NOTHING |
| Set instead of increment | "status = PAID" instead of "count = count + 1" |
| Conditional update | UPDATE orders SET status='PAID' WHERE id=501 AND status='PENDING' |
| Version check | Update only if version = 7 (optimistic locking) |
Idempotency in Message Queues
Most queues (Kafka, SQS) deliver at least once, so a consumer can get the same message twice.
1Consumer gets "OrderPlaced #501"2 → already in processed_messages table? → skip3 → else → send email → insert message ID into processed_messagesConsumers must be idempotent, or users get duplicate emails, duplicate deliveries, or double refunds.
Summary
1Idempotent = same result whether run once or many times2→ needed because retries are unavoidable3→ GET, PUT, DELETE are idempotent; POST is not4→ make POST safe with an Idempotency-Key + stored response5→ use atomic checks (Redis NX / unique constraint) for races6→ queue consumers must dedupe by message IDNext, we'll look at Rate Limiting, which controls how many requests a client can send in a given time.