Stripe Failed Payment Retry: Complete Technical Guide
Stripe's default retry logic recovers only 30% of failed payments. This technical guide shows you how Stripe retries work, what's missing, and how to build smarter retry logic that can achieve industry-leading recovery rates (85-94% based on studies).
How Stripe's Default Retry Logic Works
When a subscription payment fails in Stripe, the platform automatically attempts to retry the charge. Here's what happens:
Stripe's Default Retry Schedule
Default Retry Timeline
- Day 0: Initial charge fails
- Day 3: First retry attempt
- Day 5: Second retry attempt
- Day 7: Third retry attempt
- Day 7+ (if all fail): Subscription marked as
past_dueorunpaid
Note: You can configure this schedule in Stripe Dashboard → Settings → Billing → Subscriptions and emails.
What Happens During a Retry
- 1. Payment attempt: Stripe attempts to charge the same card that failed previously
- 2. Webhook fired:
invoice.payment_action_requiredorinvoice.payment_failed - 3. Email sent (optional): Stripe can email customers about the failure
- 4. Subscription state updated: Remains
past_dueif retry fails
The Problem with Stripe's Default Retries
Stripe's one-size-fits-all approach has major limitations:
- ❌ Ignores decline codes: Retries expired cards the same way as insufficient funds (even though expired cards will never succeed)
- ❌ Fixed schedule: Doesn't adapt to customer behavior (payday, billing cycles)
- ❌ Limited attempts: Only 3-4 retries before giving up
- ❌ No smart timing: Retries at the same time of day regardless of when the customer is likely to have funds
- ❌ Poor recovery rate: Only 30-40% of failed payments are recovered
Understanding Stripe Decline Codes
When a payment fails, Stripe returns a decline code that explains why. This is your roadmap for building smarter retry logic.
Common Decline Codes & What They Mean
| Decline Code | Meaning | Should Retry? | Action |
|---|---|---|---|
card_declined | Generic bank decline | Yes | Retry in 4h → 24h → 3d |
insufficient_funds | Not enough money in account | Yes | Retry after payday (3-7d) |
expired_card | Card expiration date passed | No | Email customer immediately |
incorrect_cvc | CVC code is wrong | No | Email customer immediately |
processing_error | Network/technical issue | Yes | Retry quickly (1h → 4h → 24h) |
do_not_honor | Bank blocked the charge | Maybe | Retry once in 24h, then email |
card_velocity_exceeded | Too many charges too fast | Yes, slowly | Wait 7d before retrying |
lost_card / stolen_card | Customer reported card lost/stolen | No | Email customer for new card |
How to Access Decline Codes
In the Stripe API, decline codes are returned in the charge object:
{
"id": "ch_3ABC...",
"object": "charge",
"status": "failed",
"failure_code": "insufficient_funds",
"failure_message": "Your card has insufficient funds.",
"outcome": {
"type": "issuer_declined",
"reason": "insufficient_funds"
}
}You can also view decline codes in the Stripe Dashboard → Payments → [Failed Payment] → "Decline reason".
Building Smart Retry Logic
To achieve the industry benchmark of 85-94% recovery (vs Stripe's 30%), you need decline code-aware retry logic.
Smart Retry Schedule by Decline Code
For insufficient_funds:
- 1st retry: 3 days (wait for payday)
- 2nd retry: 7 days (biweekly paycheck)
- 3rd retry: 14 days (monthly billing cycle)
- 4th retry: 30 days (last chance before cancellation)
Why it works: Aligns retries with when customers are likely to have funds
For card_declined (generic):
- 1st retry: 4 hours (might be daily limit issue)
- 2nd retry: 24 hours (next day, new limit)
- 3rd retry: 3 days
- 4th retry: 7 days
Why it works: Catches temporary bank limits and customer actions
For processing_error:
- 1st retry: 1 hour (technical issue likely resolved)
- 2nd retry: 4 hours
- 3rd retry: 24 hours
- 4th retry: 3 days
Why it works: Technical errors usually resolve quickly
For expired_card / incorrect_cvc:
DO NOT RETRY. Send dunning email immediately with card update link.
Why: No amount of retrying will succeed — customer must update their card
Implementation Options
You have three options for implementing smart retries:
Option 1: Configure Stripe's Retry Rules
In Stripe Dashboard → Settings → Billing → Subscriptions and emails, you can customize:
- • Number of retry attempts (1-4)
- • Days between retries
- • Email notifications to customers
Pros: Easy, no code required
Cons: Still one-size-fits-all, doesn't adapt to decline codes
Option 2: Build Custom Retry Logic with Webhooks
Listen to Stripe webhooks and trigger manual retries based on decline codes:
// Example: Node.js webhook handler
app.post('/stripe-webhook', async (req, res) => {
const event = req.body;
if (event.type === 'invoice.payment_failed') {
const invoice = event.data.object;
const charge = invoice.charge;
const declineCode = charge.failure_code;
// Smart retry logic
if (declineCode === 'insufficient_funds') {
scheduleRetry(invoice.id, 3 * 24 * 60 * 60 * 1000); // 3 days
} else if (declineCode === 'card_declined') {
scheduleRetry(invoice.id, 4 * 60 * 60 * 1000); // 4 hours
} else if (declineCode === 'expired_card') {
sendDunningEmail(invoice.customer);
}
}
res.status(200).send('OK');
});Pros: Full control, adapts to decline codes
Cons: Requires development time, ongoing maintenance
Option 3: Use a Payment Recovery Tool (Like Revive)
Tools like Revive handle smart retries automatically:
- ✅ Decline code-aware retry schedules
- ✅ Automated dunning emails
- ✅ Real-time recovery analytics
- ✅ No code required
Pros: Zero setup, designed for industry-leading recovery rates, no engineering time
Cons: Monthly cost (but ROI is typically 10-50x)
Webhook Events You Need to Track
If you're building custom retry logic, listen to these Stripe webhook events:
| Event | When It Fires | Action |
|---|---|---|
invoice.payment_failed | Payment attempt failed | Schedule smart retry based on decline code |
invoice.payment_succeeded | Payment succeeded (after retry) | Mark as recovered, stop retry loop |
customer.subscription.updated | Subscription status changed | Track if moved to past_due |
charge.failed | Individual charge failed | Extract decline code for analysis |
Example Webhook Payload
{
"type": "invoice.payment_failed",
"data": {
"object": {
"id": "in_1ABC...",
"customer": "cus_XYZ...",
"subscription": "sub_123...",
"amount_due": 2900,
"attempt_count": 1,
"charge": {
"id": "ch_3DEF...",
"failure_code": "insufficient_funds",
"failure_message": "Your card has insufficient funds."
}
}
}
}Advanced Strategies
1. Vary Retry Time of Day
A charge that fails at 2 AM might succeed at 10 AM (when banks are more lenient with daily limits). Spread retries across different times:
- 1st retry: 10 AM (morning when balances are higher)
- 2nd retry: 2 PM (afternoon)
- 3rd retry: 8 PM (evening after work hours)
2. Enable Stripe's Network Tokens
Network tokens automatically update expired card details and increase authorization rates. Enable in Stripe Dashboard → Settings → Payments → Network tokens.
Impact: Reduces expired card failures by 50%+
3. Use Card Account Updater
Visa and Mastercard automatically provide new card details when cards expire. Enable in Stripe Dashboard → Settings → Billing → Card account updater.
Impact: Reduces expired card failures by 30-40%
4. Retry with Backup Payment Methods
If you've collected multiple payment methods from customers, try the backup method when the primary fails:
// After primary card fails
if (customer.backup_payment_method) {
stripe.invoices.pay(invoice.id, {
payment_method: customer.backup_payment_method
});
}Dunning Management with Stripe
Payment retries alone won't recover expired cards or persistent failures. You need dunning emails to prompt customer action.
Stripe's Built-In Dunning Emails
Stripe can send automatic emails when payments fail. Configure in Dashboard → Settings → Billing → Emails:
- ✅ Payment failed notification
- ✅ Upcoming invoice reminders
- ✅ Subscription canceled notification
Limitation: Stripe's emails are generic and not customizable. They have low open rates (~13%) and poor conversion.
Custom Dunning Emails (Better Approach)
Disable Stripe's default emails and send your own branded emails:
- 1. Disable Stripe emails: Dashboard → Settings → Billing → Emails → Turn off
- 2. Listen to webhooks:
invoice.payment_failed - 3. Generate card update link: Use Stripe Checkout in
setupmode - 4. Send branded email: Via SendGrid, Postmark, etc.
// Generate card update link
const session = await stripe.checkout.sessions.create({
mode: 'setup',
customer: 'cus_ABC...',
success_url: 'https://yourapp.com/billing/success',
cancel_url: 'https://yourapp.com/billing'
});
// session.url is the one-click card update link
sendEmail({
to: customer.email,
subject: 'Payment update needed',
body: `Click here to update: ${session.url}`
});Monitoring & Analytics
Track these metrics to optimize your retry strategy:
- Payment Failure Rate:
(Failed payments / Total payments) × 100
Benchmark: 7-11% is average - Recovery Rate:
(Recovered payments / Failed payments) × 100
Benchmark: 85-94% with smart retries - Average Time to Recovery:
Days from failure to successful charge
Benchmark: <7 days is ideal - Recovery by Decline Code:
Which decline codes have the best/worst recovery rates?
Where to View Metrics in Stripe
- Failed payments: Dashboard → Payments → Filter by "Failed"
- Decline codes: Click on any failed payment → "Decline reason"
- Recovery trends: Dashboard → Analytics → Payment retries (if available)
Skip the Engineering Work
Revive implements smart, decline code-aware retries and dunning emails for Stripe automatically. Connect your account in 3 minutes and start recovering more failed payments.
Try Revive Free14-day free trial • No credit card required
Key Takeaways
- 💡 Stripe's default retry logic recovers only 30% of failed payments
- 💡 Decline codes tell you WHY a payment failed — use them to build smarter retries
- 💡 Expired cards should never be retried — send dunning emails immediately
- 💡 Insufficient funds needs 3-7 day delays to align with payday
- 💡 Custom retry logic via webhooks can help achieve the industry benchmark of 85-94% recovery
- 💡 Enable Card Account Updater and Network Tokens to prevent failures
- 💡 Track recovery rate by decline code to optimize your strategy
The bottom line: Stripe's default retries are a good starting point, but not optimized for maximum recovery. By understanding decline codes and implementing smart retry logic, you can recover 3x more failed payments (studies show 85-94% is achievable) and save thousands in MRR every month.
About Revive: We build decline code-aware retry logic and automated dunning for Stripe — designed to help achieve industry-leading recovery rates without writing any code. Connect your Stripe account in one click and start recovering revenue today.