How Stripe's Idempotency-Key Header Prevents Duplicate Charges

Khanh Nguyen
Khanh Nguyen
(Updated: )
Listen to this article0 / 0
Editorial illustration of two racing transaction streams converging onto a single, interlocking gateway with a keyhole vault, symbolizing exactly-once payment processing and distributed idempotency. Photo: AI/BytePith

A single dropped connection can leave a payment request in an ambiguous state. Stripe's 2017 engineering post on API design lays out the mechanism it uses to keep that ambiguity from ever turning into a duplicate charge, and the mechanism that actually enforces it sits one layer below the header most developers read about.

Three ways a single charge request can fail

In a post published February 22, 2017, Stripe engineer Brandur Leach described three distinct failure points in any networked request: the client can fail to connect at all, the connection can drop while the server is mid-operation, or the operation can succeed while the response back to the client is lost. Each leaves the client uncertain about what happened, and that uncertainty is the entire problem idempotency exists to solve.

HTTP already handles part of this. Leach's post points out that PUT and DELETE are idempotent by definition under RFC 7231: calling them twice has the same effect as calling them once. A charge-creation endpoint is a POST, though, and POST carries no such guarantee. Retrying it blindly could mean charging a customer twice for the same order.

Three failure points in a charge request, all resolved the same wayA client's charge request can fail at connection, mid-operation, or on the response; retrying with the same Idempotency-Key resolves all three the same way.Three failure points, one resolution pathBased on Stripe Engineering, "Designing robust and predictable APIs with idempotency," 2017Client sends charge requestConnection dropsbefore request arrivesServer failsmid-operationResponse is lostafter successClient retries with the same Idempotency-KeyStripe returns the original result — the charge runs once

What the Idempotency-Key header actually does

Stripe's answer is a client-generated Idempotency-Key header attached to any mutating POST call. The client picks a unique value for one logical operation and sends it with the request. On the first attempt, Stripe processes the charge and stores the outcome, success or failure, against that key. On any retry with the same key, Stripe returns the stored result instead of running the charge again, including a stored 500 error if that's what happened the first time. The retry doesn't need to know which of the three failure modes occurred. It only needs to know that resending the same key is safe.

Retrying blindly still has a cost, though, which is why Leach's post also recommends exponential backoff, waiting roughly 2^n seconds after n failures, mixed with random jitter so that a large batch of clients recovering from the same outage doesn't retry in the same instant and re-trigger the outage. Stripe's own Ruby library implements this backoff-and-jitter retry loop automatically when a call carries an idempotency key.

How a repeated request is resolved against a stored keyA new key runs the charge and stores the result; a repeated key returns the stored result without re-running the charge.One key, one outcomeNew key: request runs once and is stored. Repeated key: the stored response is replayed.Request arrives withIdempotency-Key headerServer checks the keyagainst its stored keysSame status codeand body every time

Where the guarantee actually lives

Stripe's own post describes the header and the client contract but doesn't publish the database schema behind it. For that layer, the clearest public explanation comes from Leach's own follow-up personal writeup rather than an official Stripe disclosure: a Postgres pattern built around a unique constraint on the idempotency-key column. That distinction matters. What follows describes a widely used pattern inspired by Stripe's public design, not a confirmed detail of Stripe's production system.

The pattern works because a unique constraint gives the database, not the application code, the final word on which of two near-simultaneous requests wins. A technical breakdown of the mechanics describes the race concretely: two servers receive retries of the same request within milliseconds of each other and both attempt to insert a row for the same key. Only one INSERT can succeed. The other is rejected for violating the constraint, and that losing server then reads the winner's stored row and returns it instead of processing the charge a second time. The retry-safe behavior a client experiences at the API layer is really a write conflict resolved once, deterministically, at the storage layer.

Two simultaneous inserts racing on one unique keyTwo servers issue competing inserts for the same idempotency key; the database's unique constraint allows only one to succeed.The database resolves the race, not the applicationIllustrative pattern described in third-party technical writeups, not a confirmed Stripe schemaServer A: INSERT key rowServer B: INSERT key rowUnique constraint on the idempotency-key column: only one INSERT succeedsThe winning server processes the charge onceThe losing server reads and returns the same stored result

What this means for anyone building on top of it

The practical upshot for integrators is that the API contract and the storage guarantee are two separate layers, and both matter. Sending an idempotency key with every mutating request is necessary but not sufficient on its own; it depends on the server enforcing that key with a constraint the database itself polices, not with an application-level check-then-write sequence that leaves its own gap for two requests to slip through at once. The two secondary writeups on this pattern independently flag the same two mistakes to avoid: don't build a key from a timestamp, since clock skew can produce collisions, and don't store the entire request body as the dedup record, since a normalized hash plus the minimum response state needed to replay is enough. Stripe's public documentation and Leach's own follow-up writing describe the shape of a reliable answer to a problem every payments integration eventually runs into: over an unreliable network, the only way to guarantee an operation ran exactly once is to make retrying it safe, then let one system, not the client, decide when it has already happened. Stripe's subsequent moves into adjacent infrastructure suggest the same reliability discipline extends well past card payments as the company's product surface grows.

Comments (0)

Sort by:

No comments yet.

Be the first to share your perspective on this topic.