← Back to Blog
·8 min read·Yarin Goldstein

Stripe insufficient_funds: When and How to Retry Without Annoying the Customer

insufficient_funds is the decline code most worth retrying, but the timing is everything. Here is the payday-aware retry approach I am encoding into RecoverStack, and the parts I am still unsure about.

I'm Yarin, solo founder of RecoverStack, a flat-fee dunning tool for bootstrapped Stripe SaaS, which means I have a vested interest in telling you that retrying failed payments is worth the effort. I want to name that bias up front, because insufficient_funds is the one decline code where I don't think I have to oversell it. Of all the codes I went through building the decline engine, this is the one I put the most thought into, and it's also the one where getting the timing wrong quietly costs you the recovery.

So this post is the long version of that thinking. What the code actually means, why the obvious retry approach leaves money on the table, the payday-aware window I'm encoding, and the parts I'm still genuinely unsure about.

What insufficient_funds actually tells us

insufficient_funds means the card itself & the customer are fine, the account just didn't have the balance to cover the charge at the exact moment Stripe tried to. It shows up most on debit cards, prepaid cards, and with customers who run their checking account close to zero on purpose and refill it on payday.

That makes it almost the opposite of a code like expired_card, where retrying the same card literally never works and the only path forward is getting the customer to give you a new one (I will write up about that case separately in the future). insufficient_funds is a timing problem, not a card problem. The exact same charge on the exact same card will very likely go through in a few days once the account has money in it again.

I'll say clearly what I can't cite: I don't have a primary source that ranks decline codes by volume, and Stripe doesn't publish a per-code breakdown. But from everything I've read, insufficient_funds is one of the highest-frequency transient declines in a subscription business. If you have data that says otherwise for your own account, I'd genuinely like to see it.

Why naive retries fail here

The instinct is to just retry on a fixed schedule and hope you catch the refill window. Stripe's own Smart Retries recommended default is 8 tries within 2 weeks, and the window is configurable from 1 week up to 2 months. Smart Retries picks the individual retry times with a ML model rather than a flat interval, so this isn't a dumb fixed schedule. But, the strategy still doesn't change based on the code, so every retryable failure gets the same retry-it treatment.

The problem that's specific to insufficient_funds is the account didn't empty at a random time. It emptied because the customer spent everything down to zero, and it refills on a schedule you can partially guess: the 1st of the month, the 15th, or the customer's payday. A charge that fails on the 13th, when payroll landed on the 1st and got spent, wants a retry aligned to the next inflow, not a retry on the 14th, the 16th, and the 19th against an account that's still empty at that point.

Retrying against an empty account isn't free too - each failed retry is a hard decline on the customer's statement, and enough of them in a short window can start to look like the kind of repeated-charge pattern issuers flag. So the naive approach wastes attempts AND spends goodwill. The scarce resource is well-timed attempts, and a fixed cadence spends them at the wrong moments.

The payday-aware retry window

This is the part I actually care about: Instead of a generic 72-hour bucket, I anchor the retries to the days an account is most likely to have money in it. Here's the shape of what I'm encoding, straight from the strategy in the pillar:

  • First retry: the next 1st or 15th of the month, or the next Monday, whichever comes first.
  • Second retry: the other of those two anchors, plus about a day, so overnight payroll has actually settled.
  • Third retry: 7 days after the second, in case the customer got paid mid-cycle.

As a small routing map, the scheduler decides the anchor like this:

// Illustrative: the shape of the insufficient_funds anchor logic,
// not a paste of the production file.
function nextPaydayAnchor(from: Date): Date {
  const first = nextDayOfMonth(from, 1);
  const fifteenth = nextDayOfMonth(from, 15);
  const monday = nextWeekday(from, 'MON');
  // Soonest plausible refill wins the first attempt.
  return earliest([first, fifteenth, monday]);
}

The honest caveat: this is design intent backed by industry priors, not my own recovery data yet. I have zero customers and zero cohort numbers as I write this. The old third-party figure floating around for insufficient_funds recovery (a specific "~80%" number) traces back to a source that's since gone dead, so I won't repeat it. The defensible framing is that smart timing lands somewhere in the 40%-60% range as an industry estimate, and I'm betting payday anchoring pushes toward the top of that band. I'll publish the real number on recoverstack.dev/metrics once the cohort data replaces the guesswork.

When to email, and when to stay silent

The other half of getting this code right is knowing when to say nothing.

On the first retry, I send no customer email at all. insufficient_funds usually self-resolves, and emailing someone "your payment failed" over a transient blip that fixes itself in three days just teaches them to worry about your products. The retry fires quietly, and if it succeeds, the customer never even knew there was a hiccup.

The email only enters on escalation. On the second attempt I send a short, low-alarm note: your last payment didn't go through, the next attempt is on the [date], top up or update your card if you'd rather not wait. On the third, when the payday anchors haven't worked, the tone gets more direct and the email carries a one-click card-update link, because at that point "your account keeps coming up empty" is a real signal that something needs to change.

That cadence isn't arbitrary. Baremetrics' dunning recovery data shows per-email recovery landing around 13% on day 0, 11% on day 3, 11% on day 7, and dropping to roughly 4% by day 15. The recovery curve is steep, so the escalation emails need to land inside that window, not weeks later.

Where my defaults break

Two places I already know this approach is shaky.

  • The first is geography: The 1st-and-15th anchoring assumes a US-style monthly or semi-monthly payroll. That does not hold everywhere, and the sharpest edge is actually India: per Stripe's own docs, Stripe doesn't retry India-issued cards at all, because of the RBI e-mandate regime around recurring payments. So for a chunk of an international customer base, the payday window isn't just miscalibrated, the retry itself is off the table and the whole flow has to route to a customer-action email instead.
  • The second is partial balances. If a first retry captures against a partially-funded account and only pulls part of the charge, do I still fire the second retry for the remainder, or leave it? Stripe handles partial authorization on some integrations but not by default, and I haven't decided the right behavior. That one's going to get answered by real failures, not by me reasoning about it.

What I'm still unsure about

A few open questions I don't have answers to yet:

  • How accurately can I actually estimate a given customer's refill day without ever looking at their bank? The 1st/15th/Monday heuristic is more of a guess at this point.
  • How many payday-aligned attempts should fire before I give up on retry and go all-in on the card-update email? Three is my current number.
  • Is there a version where a gentle pre-emptive heads-up (before the charge even fails, for customers with a history of insufficient_funds) beats the silent-first-retry approach? I lean to no, because it reintroduces the alarm I'm trying to avoid, but I'm not sure about that one.

I'll answer these in public as the cohort data comes in, on recoverstack.dev/metrics. If you're running dunning on Stripe today and you've already learned any of this the hard way, I would much rather hear your numbers than keep guessing at mine. Reply to any RecoverStack email or hit me at [email protected].


If you want the full code-by-code reference this sits inside, the pillar is Stripe Decline Codes: What Each One Means and What I Plan to Do About It. For the financial framing of why chasing involuntary churn is worth it at all, see How Much MRR Is Your Stripe Account Losing to Failed Payments?. And if you're weighing whether Stripe's built-in retries are enough on their own, I compared them head to head in Stripe Smart Retries vs Custom Dunning.

About the author. I'm Yarin Goldstein. 24, based in Israel, building RecoverStack solo as my first real business after 5 years of professional dev. RecoverStack is flat-fee dunning for bootstrapped Stripe SaaS, and the founding cohort is free for your first 90 days. The full story is at /about. The waitlist (40% off for life) is at /early-access, or run a free Stripe audit to see your own failure profile first.

stripeinsufficient-fundsdecline-codespayment-recoverydunningsaas