Stripe Smart Retries vs Custom Dunning Logic: What Actually Recovers More Revenue
Stripe's Smart Retries are a good baseline — but they're not a payment recovery strategy. Here's an honest comparison, with code, of what Stripe handles natively versus what custom logic recovers on top.
Stripe's Smart Retries sound like exactly what you need. Machine learning. Optimal timing. Built right into your existing payments stack. Just enable it and let it run.
For many SaaS businesses, that's where the conversation ends — and it's costing them.
Stripe Smart Retries are a good baseline. But they're not a complete payment recovery strategy. Here's an honest comparison of what Stripe handles natively versus what custom dunning logic recovers on top, with the code to back it up.
What Stripe Smart Retries Actually Do
Stripe's built-in retry logic (available in Billing settings) uses ML models to predict the optimal retry time for each failed payment. It analyzes signals like:
- Time of day and day of week payment success rates
- Customer's historical payment behavior
- Card network patterns
- Failure reason codes (
insufficient_fundsvs.card_declinedvs.do_not_honor)
When a payment fails, Stripe schedules 3–4 retry attempts over approximately 8 days. If all retries fail, the subscription moves to a configured state — canceled, unpaid, or past_due — based on your settings.
What Smart Retries get right:
- ✅ Zero engineering effort to enable
- ✅ Genuinely smarter than naive fixed-interval retries
- ✅ Handles the retry timing problem reasonably well
What Smart Retries miss:
- ❌ No email communication on failures (you configure that separately, if at all)
- ❌ No in-app notifications
- ❌ No pause flow — it's cancel or nothing
- ❌ No segmentation by customer value or plan tier
- ❌ Limited retry window (8 days default)
- ❌ No insight into what's recoverable vs. truly lost
The Recovery Gap: Where Custom Logic Wins
A typical SaaS company with no dunning strategy recovers about 20% of failed payments through customer-initiated card updates. Stripe Smart Retries alone push that to 40–45%.
A complete dunning strategy — retries + email sequences + in-app flows — recovers 70–78%.
The Math
On $5,000/month in payment failures, that 30-point gap means $750 extra recovered — every month.
Stripe only: $2,000 recovered. Full dunning: $3,750 recovered. The math scales linearly.
Layer 1: Extending the Retry Window
Stripe's default retry window is ~8 days. For some failure types — particularly insufficient_funds — that's often too short.
Consider a customer who gets paid on the 15th. Their card failed on the 10th. Stripe retries through the 18th and gives up. But if you extended the window to 21 days, the payment would succeed on the 16th.
With Stripe's API, you can implement extended retry logic by catching invoice.payment_failed webhooks and scheduling your own retry attempts:
// webhook handler for invoice.payment_failed
app.post('/webhooks/stripe', async (req, res) => {
const event = stripe.webhooks.constructEvent(
req.body, req.headers['stripe-signature'], process.env.WEBHOOK_SECRET
);
if (event.type === 'invoice.payment_failed') {
const invoice = event.data.object;
const failureCode = invoice.last_payment_error?.decline_code;
// Extend window for insufficient_funds — likely paycheck-timing issue
if (failureCode === 'insufficient_funds') {
await scheduleRetry(invoice.id, { delayDays: 5 }); // retry on payday window
}
// Immediate re-try for do_not_honor — often a bank block, resolves quickly
if (failureCode === 'do_not_honor') {
await scheduleRetry(invoice.id, { delayHours: 24 });
}
}
res.json({ received: true });
});Different failure codes warrant different retry strategies:
| Failure Code | Meaning | Optimal Retry Strategy |
|---|---|---|
| insufficient_funds | Card limit / account empty | Wait 5–7 days (payday cycle) |
| do_not_honor | Bank declined generically | Retry in 24–48 hours |
| card_declined | Card blocked or flagged | Prompt customer to update card |
| expired_card | Card past expiration | Email immediately, request new card |
| lost_card / stolen_card | Fraud block | Email immediately, no retries |
Layer 2: Email Dunning Sequences
Stripe Billing has a basic dunning email feature. It sends a single notification when a payment fails. That's better than nothing. It's not good enough.
A three-email dunning sequence consistently outperforms single-notification approaches by 20–30% in recovery rate. Here's the sequence that works:
Email 1 — Day 0 (failure day): Soft notification
Email 2 — Day 3: Gentle reminder
Email 3 — Day 6: Final notice
Keys: plain text formatting (not HTML templates), sent from a real person's address, and a single pre-authenticated link that drops them directly on the billing update page — not your login screen.
Layer 3: In-App Notifications
Email gets a 40–50% open rate on dunning messages. That means half your customers with payment issues aren't seeing your emails.
In-app notifications catch them when they actually log in:
// Check payment status on app load
const { data: subscription } = await supabase
.from('subscriptions')
.select('status, days_until_cancellation')
.eq('user_id', userId)
.single();
if (subscription.status === 'past_due') {
showPaymentBanner({
daysRemaining: subscription.days_until_cancellation,
updateUrl: generateBillingPortalUrl(userId),
});
}A simple banner at the top of your app — "Your payment failed. Update your card to avoid losing access." — recovers 10–15% more revenue on top of what email sequences capture. Entirely absent from Stripe's built-in tooling.
Layer 4: Pause Instead of Cancel
The default Stripe behavior when all retries fail: cancel the subscription.
The problem: canceling a customer creates a re-acquisition problem. They have to sign up again, re-enter their card, potentially re-complete onboarding. Friction compounds at every step, and most canceled customers don't come back.
A pause flow changes the equation:
// Instead of canceling, pause the subscription
await stripe.subscriptions.update(subscriptionId, {
pause_collection: {
behavior: 'mark_uncollectible',
resumes_at: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60), // 30 days
},
});Customers who exit via pause are 4× more likely to reactivate than customers who cancel.
Their data persists. Settings remain. One click to reactivate — no re-onboarding, no friction.
The Build-vs-Buy Decision
Building a complete dunning system — retry logic, email sequences, in-app notifications, pause flows, analytics — takes a few weeks of engineering time and ongoing maintenance as Stripe's API evolves.
The logic isn't complicated. But it's finicky. Webhook idempotency, retry deduplication, email deliverability, authenticated billing portal links — there are a dozen small things to get right.
Revive handles all of it out of the box
- ✓ Failure-code-aware retry scheduling
- ✓ Three-email dunning sequences (customizable)
- ✓ Drop-in JavaScript for in-app payment banners
- ✓ Automatic pause flows for at-risk subscribers
- ✓ Real-time recovery analytics
Connect Stripe in 15 minutes. Flat $49/month. No revenue share.
See How Revive Handles Payment Recovery →Related: How Payment Recovery Expands MRR · Stripe Failed Payment Retry: Complete Technical Guide · Dunning Email Best Practices