DEV Community

Cover image for Idempotency keys in Stripe: safe retries after lost responses

Idempotency keys in Stripe: safe retries after lost responses

An idempotency key earns its keep when the server finishes your request and the response disappears. Your checkout shows a spinner, the customer taps again, and your retry helper has to make a decision with incomplete information. Stripe documents a concrete way to keep those attempts attached to the same logical action. Understanding its boundaries matters as much as sending the header.

TL;DR

  • A timeout leaves the original outcome unknown. The server might already have completed the action.
  • Stripe API v1 saves the first request's status and response body after endpoint execution begins. A same-key retry can retrieve that saved response, including a 500 error.
  • Generate the key before the first protected attempt and persist it with the operation. Reuse the key and original parameters for that operation's retries.
  • Stripe's v1 guidance bounds unresolved network retries to the first day. Beyond that, stop and reconcile the outcome.
  • A fresh key can repeat an action. Recovering connectivity alone gives you no answer about the original side effects.

Why a lost response makes API retries ambiguous

Picture a coffee purchase. This is a conceptual example, so there is no mysterious bank charge or customer incident to investigate. Your app sends a payment request. Stripe completes the operation. The network breaks while the response is returning to your app.

The server knows what it did. Your app knows it sent something and waited. Those are different pieces of knowledge, and a timeout can leave them out of sync.

Stripe's engineering article on robust API design describes this failure case alongside failures while establishing a connection or midway through a request. The awkward part is that your client can experience a network problem in all of these situations. It cannot infer a clean business outcome from the absence of a response.

That ambiguity is where a naive retry button becomes expensive. An unprotected create operation can repeat a side effect. Sending the same business data again does not, by itself, tell an endpoint that this is another attempt at a previous action. A coffee shop can sell two identical coffees perfectly legitimately.

There is also a user-interface decision hiding inside the technical one. When the customer taps again, are they resuming the pending purchase or ordering another coffee? Your application has to decide. The key then carries the identity of the action your application chose.

What an idempotency key identifies

RFC 9110 defines idempotency in terms of intended effect: repeating identical requests has the same intended effect on the server as making one request. The definition allows incidental effects, such as logging each attempt. It also allows responses to differ.

That distinction is useful because it keeps the generic idea separate from a provider's particular contract. Stripe API v1 adds a specific response-replay behavior, described in its current idempotent request documentation. The episode and this article use that v1 scope throughout. The current documentation gives API v2 different semantics.

A client-generated key labels one logical action. Stripe suggests a UUID v4 or another sufficiently random string and advises against including sensitive information. The useful application relationship is simple: this operation has this key. It needs to survive long enough for a later attempt to find it.

Generating a new key inside every retry turns every attempt into a new identity. It defeats the relationship you were trying to preserve. Reusing a key for a genuinely new purchase creates the opposite problem: two actions are being presented as the same action.

Application intent Key choice
Resume the pending coffee purchase Keep its original key
Retry after a lost response Keep its original key and parameters
Begin a genuinely new purchase Generate a new key
Resolve an operation whose outcome remains unknown beyond the retry window Reconcile before attempting another action

This table describes application intent. It represents no private Stripe storage architecture.

How Stripe API v1 remembers a request

The v1 contract saves the status code and body of the first request after endpoint execution begins. Retries using the same key return the saved response. You can think of the retained result as a receipt attached to the operation identity.

In the successful coffee example, another copy of the receipt travels home. The original completed action stays completed. The saved result stays associated with the key. That is the relationship the motion diagram in the episode makes visible.

The documentation's inclusion of 500 errors is important. A retained error response can keep coming back with the same key. Your retry loop can therefore restore communication with the server and still receive the error it received earlier.

There is no universal delivery guarantee hiding in this header. Your application may also send email, update inventory or write to its own database. Those systems need their own failure handling and operation boundaries. Calling a payment endpoint safely does not establish the outcome of every other side effect in the workflow.

I would write an integration review around the operation mapping first. Where does the key get created? Which record owns it? Can a later request recover the same parameters? Can someone distinguish a deliberate second purchase from a resumed first purchase? These questions catch mistakes that a successful happy-path demo leaves invisible.

Persist the key before the protected attempt

The ordering matters. Create the unique key before the first protected attempt and preserve its association with the action. Attaching a key after an earlier unprotected request completed cannot retroactively protect that earlier call.

The episode explicitly resets its example when introducing the protected request. This sounds like a small visual choice, but it carries the integration rule: protection starts with the identity you send on the original protected attempt.

Stripe's advanced error-handling guide also discusses preserving a key across separate SDK calls and process restarts. A key living only in an in-memory retry closure can disappear when the application restarts. The next process then needs the durable operation record to recover the original identity.

There is no prescribed storage engine in this explanation. The requirement is continuity: the operation, key and retry parameters remain related in your records. A review should also consider what happens when two application workers encounter that record at once. The documented concurrent-request case is part of the API contract you need to handle.

Parameter mismatches and concurrent requests

Stripe API v1 compares incoming parameters with the original request's parameters. Changing them while reusing the key produces an error. A key therefore needs to travel with the original operation's parameters, rather than becoming a general-purpose permission slip to send whatever the checkout currently contains.

Two other cases have a different boundary. If parameters fail validation, endpoint execution does not begin and Stripe saves no idempotent result for that attempt. The documentation gives the same no-saved-result treatment to an attempt conflicting with another request executing concurrently. These requests can be retried.

The words “for that attempt” do real work here. A competing request's conflict does not prove that the first request completed. It also does not mean that a previously saved successful response has been erased. Keep the independent cases independent when reasoning about them.

A concurrent example has a first execution still active and a second attempt stopped by a conflict. The client must handle the conflict through the documented retry behavior. It gets no license to invent a new operation identity while the original outcome remains unresolved.

Retention, exponential backoff and the point to stop

Stripe's v1 documentation says keys can be removed after they are at least 24 hours old. This establishes a minimum age for pruning. It does not promise that every key is deleted at exactly that boundary. If the original key has been pruned, reusing it generates a new request.

For unresolved network errors, the advanced guide tells clients to retry with the same key and parameters within the first 24 hours. Beyond that, stop and reconcile. Keeping this conservative retry boundary is useful even when the provider happens to retain a particular key longer.

Exponential backoff and jitter address the pressure created by retries. Stripe's engineering article describes backing off between attempts and introducing randomness so many clients do not retry in lockstep. A struggling server benefits from breathing room. A queue outside a burning coffee shop already has enough enthusiasm.

SDK defaults deserve an explicit check. The episode says Stripe's libraries handle retries, while asking you to verify your library's defaults. Library version and client construction matter; this article supplies no universal retry count or delay that overrides the documentation for your actual client.

A cached 500 can outlive the network problem

Consider a separate hypothetical error example. The saved response is a 500. Connectivity later recovers, yet a same-key retry keeps returning that cached response. The original operation's side effects remain an independent question.

Stripe's advanced guide treats a 500 outcome as indeterminate and says it may have produced side effects. A fresh key can repeat the action. Resolve the original outcome before using a new key to try again.

The documented reconciliation options include retrieving relevant objects, inspecting the request in the Dashboard and using webhook events. Contact support when the outcome remains unresolved. These are evidence-gathering routes; the guide does not guarantee that every ambiguous operation will instantly become clear.

This is where I want an application to be honest with both its own records and the customer. “Still checking” can be a truthful state. A network recovery badge alone cannot tell the customer whether the original purchase succeeded.

Verdict: SHIP IT

I'd ship stable operation keys with bounded retries and reconciliation. The integration needs to preserve identity across attempts and recognize the point where another network call can create fresh risk. Customers should get coffee without funding your distributed systems education.

FAQ

Does a timeout mean my Stripe payment failed?

The outcome can remain unknown. The server may have completed the request before the response disappeared. Follow the documented same-key retry and reconciliation guidance for the API version you use.

Can I reuse an idempotency key with different parameters?

Stripe API v1 compares the parameters with the original request and errors on a mismatch. Keep the original key and parameters attached to the same logical action.

Is a Stripe idempotency key guaranteed to expire after exactly 24 hours?

The v1 documentation specifies keys can be pruned after they are at least that old. A pruned key can execute a new request, which is why unresolved network retries have a bounded window.

Does retrying a cached 500 with a new key fix it?

A new key can repeat the action. The original 500 may have produced side effects, so reconcile that outcome before creating another operation identity.

Sources


This article expands on an episode of **The Daily Diff, a five-minute daily video on what shipped and what broke in tech.
Watch the episode · Subscribe on YouTube · the written diff lands in your inbox every morning at thedailydiff.dev.

Video transcript and primary sources

Top comments (0)