A user opens a payment app, types in an amount, and taps "Pay." The spinner shows up. Then nothing — no success screen, no error, just a frozen loader.
So they tap "Pay" again. Or the app quietly retries in the background because it never got a response.
But here's the actual problem: what if the first request actually reached the server and the payment already went through, and only the response got lost on the way back? The second tap now sends the same payment again. Does the user get charged twice?
This isn't a rare edge case. Mobile connections drop, servers get slow under load, requests time out. Any system that moves money over a network runs into this.
The idea that fixes it
The concept here is idempotency.
An operation is idempotent if running it twice has the same effect as running it once. Send the same request again, retry, double tap, whatever, and the outcome shouldn't change.
Pressing an elevator button five times doesn't call five elevators. The building registers your request once. A payment should work the same way: the button might get tapped repeatedly, but the money should move once.
The server doesn't ignore the repeat request. It just recognizes it as a repeat instead of treating it as something new.
Why payments care about this more than most systems
If a retried request reloads a page or re-fetches some data, nobody gets hurt. Payments are different, because the thing being repeated moves real money.
A typical failure looks like this:
User
↓
Payment API
↓
Payment processed
↓
Response is lost
↓
User/app retries
↓
Same payment request reaches the server again
Look at where it breaks. The payment succeeded. The server did its job. The failure happened after that, somewhere between the server and the user's phone. From the server's side everything is fine. From the user's side, nothing happened — so retrying feels like the obvious move.
That's why the backend can't assume every incoming request is a brand-new payment. A slow network plus one retry, and someone's account gets debited twice for something they bought once. In fintech that's not a glitch, it's a refund, a support ticket, and a user who stops trusting the app.
The idempotency key
So how does the server tell "I already did this" apart from "this is new"?
With an idempotency key — a unique value the client generates and attaches to that specific payment attempt. Usually it's sent as an HTTP header:
POST /payments
Idempotency-Key: abc123
Content-Type: application/json
{
"amount": 100,
"currency": "NPR"
}
The key itself is just a unique string. The important rule is that a retry reuses the same key instead of generating a fresh one.
First attempt:
Idempotency-Key: abc123
↓
Server receives request
↓
Server processes payment
↓
Server stores the result associated with abc123
The server now has a record saying "abc123 was handled, and here's what happened."
Response gets lost, app retries with the same key:
Idempotency-Key: abc123
↓
Server receives request
↓
Server recognizes the key
↓
Does not create another payment
↓
Returns the previous result
No new charge, just a slightly late confirmation for the user. The key is the thing that lets the server link the retry back to the original attempt.
A few things worth knowing about the key
Same key plus same operation means retry. A different key means a different operation — which is what lets someone genuinely pay the same amount twice on purpose. They'd just be sending two different keys.
But same key with a different payload isn't a retry — that's a bug. Say abc123 is used for a Rs. 100 payment, then shows up again with a Rs. 500 request. The server shouldn't just return whatever it stored the first time. Stripe, for example, checks for this: same key with matching parameters returns the original response, but a mismatch returns an error instead of processing it.
The server also has to store and check these keys reliably. This is usually a database, often with a unique constraint on the key, so two identical requests arriving at nearly the same time can't both slip through.
There's a timing gap hiding in that too. What if a second request with abc123 lands while the first one is still being processed, before there's even a stored result yet? Most systems handle this with a lock on the key — the second request waits, or gets bounced with a 409 telling the client to try again shortly.
The two situations side by side
Without idempotency:
User sends payment
↓
Payment succeeds
↓
Response is lost
↓
User retries
↓
Server processes it again
↓
Possible duplicate payment
With idempotency:
User sends payment with key ABC
↓
Payment succeeds
↓
Response is lost
↓
User retries with key ABC
↓
Server recognizes ABC
↓
Previous result is returned
↓
No duplicate payment
The trigger is identical in both — a lost response and a retry. The only difference is whether the server blindly repeats the work or notices the repeat.
How you'd actually test this
You can read about idempotency all day. Testing it is different. Here's where to start if you're testing a payment flow:
- Double-tap it. Send the same request twice, back to back, same key. Only one charge should go through.
- Cut the response, not the request. Let the server finish processing, but kill the connection before the response comes back. Retry with the same key. Do you get the old result, or does it run again?
-
Reuse the key, change the amount. Send
abc123for Rs. 100, thenabc123again for Rs. 500. Does it error out, return the first result anyway, or actually process Rs. 500? - Fire both at once. Send two identical requests with the same key at the same time. See if both slip through before either one finishes.
-
Skip the header. Send the request without
Idempotency-Keyat all. Check if the API rejects it, or just runs it like normal.
Wrapping up
A request being sent twice doesn't mean the operation should happen twice. Networks are unreliable, users retry, apps retry automatically. None of that is avoidable, and none of it is unusual.
In payments, this matters even more. Sending the same payment twice can mean charging someone twice. Idempotency is simply a way to make those retries safe.
In the end, idempotency is about one simple thing: making sure a retry doesn't turn into a duplicate operation.
Top comments (0)