Hermes Wiki
Architecture/CaseStudies/stripe-idempotency-keys

Stripe Idempotency Keys

Problem + constraints

Payment APIs run over unreliable networks: a client can send a "charge this card" request, the charge can succeed on Stripe's side, and the response can still be lost to a timeout or dropped connection before the client sees it. The client's only safe-looking move — retry the request — is exactly the move that risks charging the customer twice. The constraint Stripe had to design around: retries must be safe by default, without asking every API consumer to build their own dedup logic, and without Stripe silently trusting a client's claim that "this is a retry."

Solution

Stripe's answer is the Idempotency-Key header on any mutating (POST) request. The client generates a unique key per logical operation (not per HTTP attempt) and sends it with the request. On the first request with a given key, Stripe processes it normally and stores the resulting status code and response body against that key. On any subsequent request with the same key, Stripe returns the stored result directly — without re-executing the charge — regardless of whether the first attempt succeeded, failed, or even returned a 500. The client is now free to retry blindly on any network error, because the server, not the client, is the source of truth on "has this already happened."

The subtlety worth noting: idempotency here isn't "make the operation naturally idempotent" (that's a different, harder problem for something like "charge $10"), it's "make the request idempotent" by keying on client-generated intent rather than operation content.

What to steal

  • Key generation belongs to the client, storage belongs to the server. The client doesn't need permission or a round-trip to get a key — it generates one locally (a UUID) before the first attempt, which means the very first request is already retry-safe.
  • Store the full response, not just a "done" flag. Returning the exact original status/body on retry means the client's retry logic doesn't need special-casing — it looks like the first request never failed.
  • Scope keys to logical operations, not to HTTP requests. A key represents "charge this specific order once," not "this specific TCP connection" — that's what makes it survive across retries, different connections, even different processes.
  • This pattern generalizes far past payments — anywhere retries are expected under a distributed/unreliable network (queue consumers, webhook delivery, any at-least-once messaging system) and the operation being retried has a side effect that must not double-apply.

Principal Engineer Lens

Idempotency keys are a clean example of pushing a hard distributed-systems problem (exactly-once semantics over an unreliable network) down into a small, well-scoped mechanism instead of asking every caller to solve it themselves — the kind of API design judgment that separates "it works" from "it's safe to build on." This pattern shows up constantly in Fintech and Capital Markets specifically — order-entry systems, settlement pipelines, ledger postings — anywhere a duplicate side effect is a financial-correctness incident, not just a bug ticket. Being able to name this pattern and its failure modes cold is a strong signal in that vertical specifically, which lines up well with where Mihir is aiming his own career next.

Reel Script

Setup: You hit "pay" on a checkout page, the spinner hangs, and you don't know if it went through. Do you click it again? That's the exact problem idempotency keys exist to solve — and it's a problem every payment API has to answer one way or another.

Concept walkthrough: Explain the header: the client makes up a unique key before it even sends the request, attaches it to the "charge this card" call. Stripe processes it once, and remembers the outcome tied to that key — success, failure, whatever it was. Any retry with that same key just gets handed back the original result instead of running the charge again.

Real example tie-in: Walk through the timeout scenario concretely: request goes out, charge succeeds on Stripe's side, network drops before the response comes back, client sees a timeout and retries with the same key — customer gets charged exactly once, not twice, even though from the client's point of view it looked like the first request vanished.

Tradeoffs & alternatives: Contrast with the alternative — making the operation itself naturally idempotent (e.g., "set balance to X" instead of "add X") — which works for some operations but not "charge a card," where the action itself isn't repeatable-safe on its own. Note the storage cost tradeoff too: Stripe has to keep these keys around for some retention window, not forever.

Principal Engineer takeaway: This is a masterclass in taking a hard problem — safe retries over an unreliable network — and collapsing it into one header and one storage table, instead of pushing that complexity onto every API consumer. That instinct, solve it once at the platform layer, is exactly the kind of thing that reads as senior/Principal judgment in a design review, especially in payments-adjacent domains where a duplicate charge is a real incident, not a bug ticket.

Sources:

Hermes Wiki