DEV Community

DevToolsmith
DevToolsmith

Posted on

The SaaS Revenue Leak: How Failed Payments Are Silently Killing Your MRR

Every SaaS founder knows about voluntary churn — customers who cancel because they're not seeing value. But there's a second, quieter type of churn that often goes untracked: involuntary churn from failed payments.

Industry data puts it at 30-40% of all subscription cancellations. For a $100K MRR business, that's roughly $3,500-$4,000 disappearing every month from credit cards that expired, bank fraud blocks, or exceeded limits — not because the customer wanted to leave.

Why Banks Decline Payment Attempts

Understanding why payments fail is the first step to recovering them:

Reason Frequency Recoverable?
Insufficient funds ~38% Yes (retry end of month)
Card expired ~25% Yes (update request)
Fraud prevention block ~18% Yes (customer contacts bank)
Card reported lost/stolen ~12% No (new card needed)
Bank-side technical error ~7% Yes (retry within 24h)

The key insight: most failures are temporary. A retry at the right time recovers the revenue.

The Optimal Retry Schedule

Don't retry immediately and aggressively — that triggers fraud detection and makes the situation worse. The optimal dunning schedule:

  • Day 0: Immediate retry (catches temporary errors)
  • Day 3: Second retry (most cards refreshed by now)
  • Day 7: Third retry with email notification
  • Day 15: Final attempt + escalation email

Each retry should use intelligent payment routing — try a different payment processor if the primary fails, or use card updater services to get updated card details automatically.

Building a Dunning Email Sequence

The email sequence matters as much as the retry schedule. Tone progression:

Email 1 (Day 3 — No-blame)

Subject: Quick update on your [Product] subscription

Hi {{name}},

We had trouble processing your payment for [Product]. This happens
occasionally — usually a temporary bank issue.

We'll automatically retry in a few days. No action needed.
Enter fullscreen mode Exit fullscreen mode

Email 2 (Day 7 — Helpful)

Subject: Action needed: Update your payment details

Hi {{name}},

We've been unable to process your [Product] subscription payment.
Your account will remain active for 7 more days.

Update your payment details here: [LINK]
Enter fullscreen mode Exit fullscreen mode

Email 3 (Day 14 — Urgency)

Subject: Your [Product] account will be suspended tomorrow

Hi {{name}},

This is a final reminder. Your account will be suspended in 24 hours
unless you update your payment details.

We'd hate to lose you — here's a one-click update link: [LINK]
Enter fullscreen mode Exit fullscreen mode

The three-email sequence typically recovers 18-25% of failed payments before the account suspends.

Implementing Retry Logic with Stripe

// Stripe webhook handler for payment failures
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = stripe.webhooks.constructEvent(
    req.body,
    req.headers['stripe-signature'],
    process.env.STRIPE_WEBHOOK_SECRET
  );

  if (event.type === 'invoice.payment_failed') {
    const invoice = event.data.object;
    const attemptCount = invoice.attempt_count;

    // Schedule next retry based on attempt number
    const retryDelays = [0, 3, 7, 15]; // days
    const nextDelay = retryDelays[attemptCount] || null;

    if (nextDelay !== null) {
      await scheduleRetry(invoice.subscription, nextDelay);
      await sendDunningEmail(invoice.customer, attemptCount);
    } else {
      await suspendSubscription(invoice.subscription);
      await sendFinalNotice(invoice.customer);
    }
  }

  res.json({ received: true });
});
Enter fullscreen mode Exit fullscreen mode

Measuring Recovery Rate

Track these metrics to optimise your dunning process:

  • Recovery rate: % of failed payments recovered before suspension
  • Recovery revenue: total MRR recovered per month
  • Time to recovery: median days from failure to successful payment
  • Email-to-update CTR: click-through on payment update links

A well-implemented dunning system typically achieves 15-25% recovery rate. Tools like ChurnGuard automate the entire process — retry scheduling, email sequences, recovery tracking — with zero code integration required.

Don't let payment failures become silent cancellations. The revenue is there; you just need to ask for it at the right time.

Top comments (2)

Collapse
 
riya_sharma_0c20080ba090c profile image
Riya Sharma •

This is such an overlooked area

A lot of teams focus on retries and dunning but still miss the user side of the story

From the dashboard it looks like churn or failed payments
But you don’t really know what the user experienced in that moment

Tools like Flidget can help add that missing context
So you can understand whether it was a payment issue confusion or actual intent to leave

Feels like combining both could significantly improve recovery rates

Collapse
 
swipeagain profile image
Swipe Again •

The decline-reason table is the most useful part of this post — especially the "Recoverable?" column, because it forces the real design question: what should happen differently for each row. Too many retry implementations treat all failures as one queue.

One refinement on the retry schedule: Day 0 immediate retries are fine for bank-side technical errors, but for insufficient_funds an immediate retry rarely helps, and a second attempt hours later can trip velocity-based fraud filters. Timing the retry to the reason matters more than the exact day count — end-of-month for funds, within 24 hours for technical errors, and stop entirely for lost/stolen.

Also worth saying about the metrics section: recovery rate on its own can mislead. A flat 20% looks fine until you split it by decline reason and discover you're recovering 60% of expired cards but 0% of fraud blocks, because nobody's telling those customers to call their bank. Segmenting recovery by reason is what turns the metric from a vanity number into an actual roadmap.

Good breakdown overall — the tone progression in the email sequence (no-blame → helpful → urgency) matches what I've seen work too.