Stripe Payment Retry Logic: Network Failures vs. Declined Cards
September 26, 2026
What Stripe Actually Retries (and What It Doesn't)
I used to assume Stripe retried failed payments the way a good assistant would: quietly, in the background, until something worked. Wrong. Stripe retries network-level failures. 5xx errors, timeouts, DNS failures. That's it.
Per Stripe's API documentation, if a card is legitimately declined for insufficient funds, a CVV mismatch, or because it's lost or stolen, Stripe reports that once and stops. No second attempt, automatic or otherwise.
That distinction is basically the whole article. I've seen retry logic that treats every failure the same: catch the error, wait a bit, try again. On a declined card, that's not resilience. That's annoying the customer three times instead of once, for a charge that was never going through in the first place.
Your business rules decide what happens next on a genuine decline, not Stripe. Prompt for a new card. Escalate to a human. Let it go. Any of those can be right, but it should be a decision you made on purpose, not a side effect of a backoff loop that can't tell a declined card from a dropped connection.
Idempotency: The One Thing That Stops Double-Charges
Here's the part that trips up almost every retry implementation I've reviewed: a timeout is not a failed charge. It's an unknown outcome.
Stripe's idempotent requests documentation is blunt about this: retry a charge with the same idempotency key, and Stripe hands back the original result instead of charging again. That key is a unique ID you attach to one specific attempt. If a request times out before you get a response, you genuinely don't know whether the charge went through on Stripe's side. Retrying with the same key is how you find out, instead of guessing and creating a second charge.
Skip the idempotency key, retry a request that actually succeeded but never sent you a response, and you will double-charge the customer. Not "might." Will. I've watched this exact failure trigger chargeback disputes on stores with zero retry logic, just a customer double-clicking a slow checkout button and the backend treating each click as a fresh charge. Idempotency keys fix both versions of this problem: the automated retry and the impatient click.
Network Failure vs. Declined Card: Why the Difference Is Everything
Two failure types that look nearly identical in a log file behave nothing alike in practice.
A 5xx from Stripe. A connection timeout. A DNS failure. The card was never even evaluated. Nothing about the customer's payment method caused this. Safe to retry after a short delay.
A 402 card declined response is a different animal. The card was evaluated and rejected. Retry it immediately and it fails again, for the same reason, every single time. Per Stripe's docs, these aren't the same category of problem, and treating them as one is where most retry systems fall apart.
Conflate the two and your system hammers a genuinely declined card three or four times in quick succession. Best case, you've wasted effort and mildly annoyed someone who already knows their card is maxed out. Worse case, that pattern of rapid repeated attempts on the same card reads exactly like fraud to Stripe's risk systems, and you've flagged a real customer's account for behavior your own code caused.
Exponential Backoff: The Timing That Actually Works
For the failures that are genuinely safe to retry, timing matters more than most implementations account for.
Immediate retries waste effort, because whatever caused the transient failure, brief packet loss, an edge node restarting, usually hasn't cleared yet. Wait a bit and give it a real chance to resolve, without adding delay the user actually notices.
A pattern I'd trust: 3 to 5 retries over roughly 30 to 90 seconds, then stop. After that, hand it off to something async, a background job, a webhook-driven check, a support queue, rather than leaving the customer staring at a spinner forever. Exponential backoff doesn't mean doubling forever. It means a few steps, then a deliberate stop. Most transient network issues clear by the second attempt anyway.
If you're building retry logic across a broader integration and not just checkout, the same backoff principles carry over to other APIs. I went deeper on that in API Integration Mistakes That Cause the Most Support Headaches.
Webhook Deduplication: The Trap That Causes Double-Crediting
This is the part generic retry tutorials almost never mention, and it's caused more real damage on projects I've worked on than the charge-retry problem itself.
Per Stripe's webhook best practices, Stripe keeps retrying webhook delivery, for up to 3 days, if your endpoint doesn't respond with a 200. That's good design on Stripe's end. Your server being briefly down doesn't cost you the event. But it also means the same event can land on your server more than once.
If your handler for charge.succeeded isn't idempotent, a duplicate delivery means you credit the order, mark it paid, send the confirmation email, and update inventory a second time. That's not a Stripe bug. That's a gap in your code. Two events with the same ID are one event, and your handling needs to treat them that way.
The fix is simple to describe and easy to skip under deadline pressure: store the event ID the first time you process it, then check for that ID before processing anything new. If it's already there, skip it. On an API integration project I built with heavy webhook handling, that one check was the difference between a system that stayed accurate under retries and one that quietly drifted out of sync. I covered the fuller picture of how webhooks work end to end in Webhooks Explained: How Your Tools Actually Talk to Each Other.
3D Secure, Declined Cards, and When to Stop Retrying
3D Secure adds a wrinkle that no amount of retry logic solves. Per Stripe's 3D Secure documentation, some cards require the customer to authenticate directly, usually through their bank's app or a one-time code. Retry the charge request without prompting for that authentication and it just fails again. Not a network problem, not a timing problem. A UX problem wearing a retry problem's clothes.
Same logic applies to genuine declines. Retrying is a business decision, not a technical one. Retry once with a gentle prompt for a different card. Or escalate straight to a human. Both are defensible. What isn't defensible is a loop that retries blindly because it can't tell "the network hiccupped" from "the bank said no."
The question worth asking about any failure in your payment flow is simple: does this need an automatic retry, or does it need a person, or the customer, to make a call? Network failures: retry. Timeouts: retry, carefully, with an idempotency key. Declined cards and 3D Secure: almost always a human or UX decision. Most of the misclassified failures I've seen in retry systems land in that second bucket.
If your checkout is bleeding revenue to failed transactions and you're not sure which bucket most of your failures fall into, figure that out before you write another line of retry code. What's your system actually doing right now when a charge fails? Guessing, or checking?
FAQ
Can I safely retry a charge request that timed out?
Only with an idempotency key. Include the same key on the retry. Stripe returns the original result if the charge succeeded, or a fresh attempt if it failed. Without the key, you risk double-charging.
Should I retry a card declined message?
Not automatically. A decline is a business decision, not a transient failure. Retrying immediately will fail for the same reason. Instead, prompt the customer for a different card or escalate. Blind retries erode trust.
How do I stop processing duplicate webhook events?
Store each event ID in your database the first time you process it. On any subsequent delivery of the same event, skip it. Stripe may retry for 3 days; deduplication prevents double-crediting.
What's the best retry delay for network failures?
Start at 10 to 30 seconds, then increase between retries (exponential backoff). Retry 3 to 5 times total over 1 to 2 minutes. After that, escalate to async handling. Most transient issues are gone by attempt 2.