You retry a failed Stripe request using the same idempotency key—exactly as the docs recommend for network errors—and you get the same 500 back. It looks like the retry did nothing. It’s the idempotency cache working as designed, and a 500 is a special case worth understanding before you decide what to do next.

How the cache works

When Stripe receives a POST with an idempotency key, it saves the response status code and body after execution begins—regardless of whether the request succeeds or fails. Every subsequent request with that key, while the original result is still retained and the parameters match, gets the stored response replayed directly. That’s the whole point: it lets you safely retry a charge creation after a dropped connection without worrying that two charges will go through.

The trouble with a 500 is that Stripe caches that outcome too. So retrying with the same key will usually reproduce the same error, not trigger a fresh attempt.

Why you can’t just swap in a new key

The natural instinct is to generate a new idempotency key and try again. Stripe explicitly advises against this because the original request’s outcome is indeterminate—the server error doesn’t mean nothing happened. A mutation may have partially executed or produced side effects before the 500 was returned. Sending the same request under a new key risks creating a duplicate object or charge.

Stripe’s engineers examine failed requests during incidents and attempt to reconcile any mutations that produced 500s. For something like a charge that sent data to a payment network before failing, they’ll try to roll it forward; if the data never left, they’ll try to roll it back. If reconciliation succeeds, Stripe will attempt to fire webhooks for any objects created as part of that process—even though the cached idempotency response won’t change. Your 500 cached response stays a 500, but the underlying state of the object may eventually resolve.

What the cache does and doesn’t cover

Two examples where no result gets cached at all, so a retry is safe: if the request fails parameter validation before execution starts, or if it conflicts with a concurrently running request, Stripe does not store an idempotent result. Those you can retry freely with the same key.

Keys can be removed automatically after they’re at least 24 hours old. If the original key has been pruned, reusing it generates a fresh request rather than a cached replay. While a key is still retained, Stripe will error if you reuse it with different parameters, so this isn’t an escape hatch while the original key is still retained.

In practice

If you hit a repeated 500 on the same idempotency key, the right posture is to treat the original request’s outcome as unknown until you get confirmation through a webhook or by inspecting the relevant object in the API. Don’t retry with a new key while that ambiguity exists. The 500 cache can persist while the key is still retained (keys can be removed automatically after they’re at least 24 hours old), and during that window Stripe may still fire webhooks as part of its reconciliation process.

If you need to build automated handling, your code should treat idempotency-key 500s as indeterminate rather than automatically retrying them with a new key—a position consistent with Stripe’s own framing that 500s during incidents are the most likely place you’ll see this.