Have you ever wondered:
"Why don't payment apps charge me twice if I accidentally click the Pay button multiple times?"
Or
"What happens if my internet disconnects while making a payment?"
The answer is Idempotency.
If you're learning backend development, this is one of those concepts every backend engineer should understand.
Let's learn it from scratch.
π€ The Problem
Imagine you're buying a laptop online.
You click the Pay button.
Client
|
|------ Pay βΉ50,000 ------>
|
Server
The server starts processing your payment.
But suddenly...
- Your internet becomes slow.
- The server successfully processes the payment.
- The response never reaches your phone.
Now your app thinks:
"Maybe the payment failed."
So it retries automatically.
Client
Pay βΉ50,000
------------>
(No response)
Retry
------------>
Retry Again
------------>
Now the server receives three identical requests.
β Without Idempotency
The backend processes every request independently.
Request 1
Charge βΉ50,000 β
Request 2
Charge βΉ50,000 β
Request 3
Charge βΉ50,000 β
Result:
Customer Paid βΉ150,000 β
Even though the customer clicked only once.
This is one of the biggest problems backend systems need to solve.
Why Do Duplicate Requests Happen?
Duplicate requests happen all the time.
- User double-clicks the button.
- Internet connection becomes slow.
- Mobile network reconnects.
- Browser refreshes.
- HTTP client automatically retries.
- Load balancer retries.
Your backend must handle these situations safely.
β The Solution: Idempotency
Idempotency simply means:
Sending the same request multiple times should have the same effect as sending it once.
The backend should process the request only once.
Every duplicate request should simply return the previous response.
How Does It Work?
Every request carries a unique value called an Idempotency Key.
Example:
POST /payments
Idempotency-Key: abc123
Think of it as a unique tracking number for a request.
First Request
The frontend sends:
POST /payments
Idempotency-Key: abc123
Backend receives it.
It asks:
Have I seen "abc123" before?
Database:
No
Since it's a new request, the backend:
- Charges the customer
- Creates the order
- Stores the key
- Stores the response
Database:
Key Response
-----------------------------
abc123 Payment Successful
Finally it returns:
{
"status": "success",
"orderId": 101
}
Retry Happens
Because of a network timeout, the client retries.
It sends the same key.
POST /payments
Idempotency-Key: abc123
Backend checks again.
Have I seen abc123?
Database:
YES β
Instead of charging again...
The backend simply returns the stored response.
{
"status": "success",
"orderId": 101
}
No duplicate payment.
No duplicate order.
No duplicate database entries.
Visual Flow
First Request
Client
|
| Pay βΉ1000
| Key = abc123
|
v
Server
Have I seen abc123?
|
No |
|
Process Payment
Save
abc123 -> Payment Success
Return Response
----------------------------------------
Retry
Client
|
| Pay βΉ1000
| Key = abc123
|
v
Server
Have I seen abc123?
|
Yes
|
Return Stored Response
(No Payment Again)
Who Generates the Idempotency Key?
Usually, the frontend (React, Angular, iOS, Android, etc.) generates it.
Example:
const key = crypto.randomUUID();
This key is attached to the request.
β οΈ A Common Beginner Mistake
Many people think:
"If the request fails, I'll generate another key."
That's wrong.
Suppose the first request uses:
abc123
The retry uses:
xyz999
The backend sees two completely different keys.
abc123 β
xyz999 β
It thinks:
"These are two different payment requests."
So it charges twice.
Idempotency completely fails.
β The Correct Way
Generate the key once.
Reuse it for every retry.
const key = crypto.randomUUID();
try {
await sendPayment(key);
} catch {
// Retry with SAME key
await sendPayment(key);
}
Notice something?
const key = crypto.randomUUID();
is executed only once.
Frontend Example
async function pay() {
// Generate only ONCE
const key = crypto.randomUUID();
try {
await sendPayment(key);
} catch {
console.log("Retrying...");
// Retry using SAME key
await sendPayment(key);
}
}
async function sendPayment(key) {
return fetch("/api/payment", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": key
},
body: JSON.stringify({
amount: 1000
})
});
}
Backend Pseudo Code
@app.post("/payment")
def payment(request):
key = request.headers["Idempotency-Key"]
# Have we already processed this request?
if database.contains(key):
return database.get_response(key)
# First time seeing this request
payment = charge_customer()
database.save(
key=key,
response=payment
)
return payment
That's the entire idea behind idempotency.
Where Is the Key Stored?
Usually in a database.
Example:
+----------------------------------------+
| Idempotency Table |
+----------------------------------------+
Key Response
------------------------------------------
abc123 Order #101
xyz456 Order #102
mno888 Payment Failed
------------------------------------------
Many production systems use Redis because it's much faster than querying a database.
Which HTTP Methods Need Idempotency?
GET
GET /users/1
Already idempotent.
Calling it 100 times doesn't change anything.
PUT
PUT /users/1
Updating the same resource multiple times results in the same final state.
Already idempotent.
DELETE
DELETE /users/1
Deleting an already deleted resource still leaves the system in the same state.
Also idempotent.
POST
POST /payments
POST usually creates a new resource.
Calling it multiple times creates multiple records.
That's why POST endpoints commonly use Idempotency Keys.
Where Is Idempotency Used?
Almost every payment platform.
- Stripe
- Razorpay
- PayPal
- PhonePe
- Google Pay
- Amazon Pay
It's also widely used in:
- Order Creation
- Flight Booking
- Hotel Booking
- Wallet Recharge
- Subscription Creation
- Invoice Generation
Basically...
Anywhere duplicate processing would cause serious problems.
Real-Life Analogy
Imagine you're ordering pizza.
You call the restaurant.
"One Large Pizza."
The phone disconnects.
You don't know whether they heard you.
You call again.
Without idempotency:
Pizza
Pizza
Pizza
Three pizzas arrive.
With idempotency:
The restaurant checks your order number.
"We've already received Order #123."
No new pizza is prepared.
Key Takeaways
β One business operation = One Idempotency Key
β Every retry uses the same key
β The backend stores the key after processing
β Duplicate requests return the previous response
β No duplicate payments
β No duplicate orders
Final Thoughts
Idempotency is one of those backend concepts users never noticeβbut they benefit from it every day.
The next time your payment succeeds after a network timeout, or you don't get charged twice after accidentally clicking Pay multiple times, you'll know there's an idempotency mechanism working behind the scenes.
Good backend systems aren't just about making things work.
They're about making sure they can't accidentally work twice. π
Top comments (1)
Great breakdown β the payment side (PIS) gets most of the idempotency attention, but the same pattern matters just as much on the read side.
When you poll a bank's transaction API to sync balances into a budget or accounting tool, you hit a subtle deduplication problem: the same transaction can show up across multiple sync cycles (a retry fires after a timeout, or the API paginates differently). Request-level idempotency keys protect the HTTP call, but the real dedup work happens at the data layer β matching on the bank's stable transaction ID plus booking date, not just "did this request succeed."
That's why bank-data aggregators (Plaid, Tink, GoCardless, Enable Banking) return transaction IDs alongside amount, payee, and date. Your sync logic should key off those, not off request success.
One edge case that catches teams off guard: some European banks return different (or no) transaction IDs across calls for the same transaction, so you end up building fuzzy matching on amount + date + description as a fallback. Worth designing for early.
Disclosure: I'm affiliated with open-banking.io, a cert-free EU bank-data API.