The hardest problem in payments isn't taking money. It's knowing, after the network dies mid-request, whether you already did. Get that wrong and you either charge a customer twice or not at all — and both are unforgivable.
Stripe's entire reputation rests on a request succeeding exactly once. But the internet offers no such guarantee. A client sends "charge $50," the server charges it — and then the response vanishes into a dead connection. The client is left in the worst possible state: it has no idea whether the charge happened. Retry, and you might double-charge. Don't, and the payment might be lost.
When you send a request over a network and get no answer, you've learned almost nothing. Maybe the request never arrived. Maybe it arrived, succeeded, and only the reply got lost. From the client's side these look identical — silence — but they demand opposite actions: retry vs. don't.
For reading data, you just retry; asking twice is harmless. For changing something — charging a card, transferring money — a blind retry is dangerous, because if the first attempt actually worked, the second one does it again. This is the core hazard of any operation that has a side effect.
Why "just check first" doesn't save you
You might think: before retrying, ask the server whether the charge went through. But that check is also a network request that can fail, and there's an unavoidable gap between checking and acting in which things change. You can't close the race by adding more round-trips. You need the operation itself to be safe to repeat.
Stripe's answer is the idempotency key. The client generates a unique key once, before sending, and attaches it as an Idempotency-Key header. The server's contract: the first time it sees a key, it performs the operation and stores the result alongside that key. If it ever sees the same key again, it skips the work entirely and replays the stored result.
POST /v1/charges Idempotency-Key: a1b2c3-unique-per-attempt # server logic if key in store: return store[key] # replay — do NOT charge again result = perform_charge() # first time only store[key] = result return result
Now retrying is not just allowed — it's correct. A client that loses the response can hammer the same request with the same key as many times as it likes; the money moves exactly once. "Idempotent" simply means: doing it again has the same effect as doing it once.
The other half: how you retry
Idempotency makes retries safe; it doesn't make them polite. If a Stripe outage causes every client worldwide to retry at the same instant, the recovering servers get flattened by a synchronised stampede — a retry storm. So Stripe pairs idempotency with exponential backoff and jitter: wait progressively longer between attempts (1s, 2s, 4s…), plus a small random offset so clients don't all retry in lockstep. Backoff spreads the retries out in time; jitter de-synchronises them.
Worth knowing
The two ideas are a matched pair, and people routinely implement only one. Idempotency without backoff is safe but can DDoS your own recovering service. Backoff without idempotency is gentle but still double-charges. Correct retry behaviour needs both: a key so repeats are harmless, and backoff-with-jitter so repeats don't pile up.
The gap it reveals
"Use idempotency keys" is easy to recite. The depth is understanding why they're unavoidable — that network ambiguity (success vs. lost-response are indistinguishable) cannot be resolved by checking, only by making the operation replay-safe — and that idempotency must be paired with jittered backoff to avoid retry storms. That full picture is the difference between knowing the term and being trusted to design a payments API.
In the interview room
Any design touching payments, orders, or writes invites: "what happens if the client doesn't get a response and retries?" The complete answer is idempotency keys for safe replay plus exponential backoff with jitter to prevent retry storms. Volunteering both — and explaining that you can't resolve the ambiguity by checking — is a strong senior signal.
The reframe
Distributed systems don't fail by giving you wrong answers. They fail by giving you no answer, and leaving you to guess. The engineers who build trustworthy systems stop trying to eliminate that uncertainty — it can't be eliminated — and instead design operations that stay correct despite it. An idempotency key is what "correct despite uncertainty" looks like in code.
You can't always know if it worked. So build the thing that's safe to do twice.
Primary source →
stripe.com — Designing Robust and Predictable APIs with Idempotency