Published · 6d agoBuild3 min read
The Retry Is Not An Edge Case: Why Payment Writes Need Server-Side Idempotency
A lost response looks exactly like a failed request from the client. That single ambiguity is why any API that moves money has to treat a duplicate submission as normal traffic.
Written for builders.See today for builders
What happened
- A user opens a payment app, taps Pay, sees a frozen loader with no success screen and no error, and either taps Pay again or the app retries in the background because it never got a response.
- The actual problem is that the first request may have reached the server and the payment already went through, with only the response lost on the way back; the second tap then sends the same payment again.
- In the typical failure the payment succeeded and the server did its job; the failure happened afterwards, between server and phone. From the server's side everything is fine; from the user's side nothing happened, so retrying feels like the obvious move.
- Because a successful write with a lost response is indistinguishable on the client from a failed write, the client cannot resolve the ambiguity and retry must be treated as the default case, leaving the server as the only place duplicate detection can happen.
- This is not a rare edge case: mobile connections drop, servers get slow under load, requests time out, and any system that moves money over a network runs into it.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A user taps Pay, the spinner hangs, no success screen and no error arrive, so the user taps Pay again or the app retries quietly in the background [1]. The failure that costs money is not the request that died in transit; it is the one that reached the server, moved the funds, and then lost its response on the way home [2].
From the server's side, that transaction is complete and correct [3]. From the phone's side nothing happened at all, which makes retrying the obvious move [3]. The client has no way to tell the two states apart, so retry is not an exception path to be handled later; it is the default case, and the only place the distinction can be resolved is the server [4]. Mobile connections drop, servers slow under load, requests time out, and any system that moves money over a network meets this [5].
The property that fixes it is idempotency: running an operation twice has the same effect as running it once [6]. The dev.to explainer uses an elevator button, pressed five times, that still summons one elevator [7]. Worth noting what that does not mean: the server is not ignoring the second request, it is recognising it as a repeat rather than as something new [8]. A retried page load or data fetch hurts nobody; a repeated payment moves real money [9], and a slow network plus one retry can debit an account twice for a single purchase, which in fintech means a refund, a support ticket, and a user who stops trusting the app [10].
The mechanism is an idempotency key, a unique string the client generates for a specific payment attempt and usually sends as an HTTP header, such as `Idempotency-Key: abc123` on a POST to /payments [11]. The operative rule is client-side discipline: a retry must reuse the same key rather than mint a fresh one [12]. On the first attempt the server processes the payment and stores the result against that key [13]; when the retry arrives, the server recognises the key, creates no second payment, and returns the stored result, so the user gets a late confirmation instead of a second charge [14]. A different key means a different operation, which is how someone deliberately pays the same amount twice [15].
Two implementation details carry most of the weight. Same key with a different payload is not a retry but a bug, and per the same write-up Stripe returns an error on parameter mismatch rather than replaying the stored response [16]. And keys have to be stored and checked reliably, typically in a database with a unique constraint, so two near-simultaneous identical requests cannot both slip through [17]. There is still a window where a second request lands while the first is in flight and no result exists yet; most systems put a lock on the key, making the second caller wait or bounce it with a 409 telling the client to retry shortly [18].
Watch where the discipline actually breaks in your own stack: whether the key survives an app restart or a background retry, whether your payload comparison is real or nominal, and whether the concurrent-arrival case is covered by a lock or by hope [12][16][18].
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
A user opens a payment app, taps Pay, sees a frozen loader with no success screen and no error, and either taps Pay again or the app retries in the background because it never got a response.
ReportedView cited source - [2]
The actual problem is that the first request may have reached the server and the payment already went through, with only the response lost on the way back; the second tap then sends the same payment again.
ReportedView cited source - [3]
In the typical failure the payment succeeded and the server did its job; the failure happened afterwards, between server and phone. From the server's side everything is fine; from the user's side nothing happened, so retrying feels like the obvious move.
ReportedView cited source - [5]
This is not a rare edge case: mobile connections drop, servers get slow under load, requests time out, and any system that moves money over a network runs into it.
ReportedView cited source - [6]
An operation is idempotent if running it twice has the same effect as running it once; a retry or double tap should not change the outcome.
ReportedView cited source - [7]
The article's analogy: pressing an elevator button five times does not call five elevators, because the building registers the request once.
Sources & coverage · 1 publisher
The reporting this story was synthesized from, earliest first. Every link goes to the original.
- dev.toSuyash DhakalAug 16Same Request Sent Twice: How Idempotency Prevents Duplicate Payments
Cited in this coverage: dev.to explainer by Suyash Dhakal
Cited in this coverage: dev.to explainer, citing Stripe's behaviour
- dev.toSaurav Pandey5d agoIdempotency: The Secret to Preventing Double Payments and Network Glitches

