HomeResourcesCase study
Case study

Stripe's Hardest Problem Is Saying "It Worked"

By The SDL team·5 min read·Updated Sep 16, 2026

When the network swallows the response, success and failure look identical. Idempotency is how you stay correct anyway.

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.

Plain English

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.

Now the engineering

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 two-generals problem with real money client charge $50 server: charged OK response lost (network) client never heard back. Did it work? Retry? Naive retry = charged twice. The fix: client attaches an Idempotency-Key it generates once. Server records the key + result. First time: do the work, save it. Any retry with the same key: skip the work, replay the saved result. The retry is now safe — same key, same outcome, charged once. And retry with exponential backoff + jitter: don’t all retry at once (that’s a thundering herd). Wait 1s, 2s, 4s… plus a random nudge, so a recovering server isn’t flattened by synchronised retries.
Idempotency turns an unsafe retry into a safe one. The key lets the server recognise a repeat and replay the original result instead of re-charging.

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

Want feedback on your design?

The weekly teardown

One real-world architecture, every week.

How real companies actually built it: the design, the trade-offs, and what to say about it in an interview. Free, and one click to unsubscribe.

Related articles