Series: Running a Subscription Business · Part 2 of 4
- Peak Trading Readiness: How We Prepare a Subscription Platform for Valentine's Day
- Involuntary Churn: The Engineering Behind Recovering Failed Subscription Payments
- SCA, 3D Secure 2 and Recurring Payments: An Architect's Guide for Subscription Platforms
- Winning the Pennies Unsung Hero Award: What Micro-Donations Mean in Tech
Contents
The Churn Nobody Chose
Subscription businesses spend a lot of energy on voluntary churn: the customer who cancels because of price, quality or a change in circumstances. Much less attention goes to involuntary churn, where the customer never decided to leave. Their card expired, was reissued after a fraud alert, hit its limit on the wrong day, or was declined by an issuer's risk model.
In my experience this is one of the highest-return areas an engineering team can work on in a subscription business. The customer still wants the product. You just need to get paid without annoying them.
Why Payments Fail
Not all declines are equal, and the single most important thing your system can do is tell them apart.
Soft declines are temporary. Insufficient funds, a generic "do not honour", an issuer system being unavailable or a velocity limit. The same card may well succeed later.
Hard declines are permanent for that card. Lost or stolen card, closed account, invalid card number, or an explicit instruction from the issuer not to try again. Retrying these achieves nothing, annoys issuers and increasingly costs money: the card schemes now have rules and fees for excessive retries, and Visa and Mastercard both publish guidance on which decline responses must never be retried.
Then there is a third category that is easy to miss: authentication required. Under Strong Customer Authentication in the UK and EEA, an issuer can decline an off-session payment and ask for the cardholder to authenticate. No amount of retrying fixes that. You need to bring the customer back on-session.
| Decline type | Examples | Retry? | Right response |
|---|---|---|---|
| Soft: funds | Insufficient funds, over limit | Yes, timed | Retry around likely pay dates; gentle reminder |
| Soft: technical | Issuer unavailable, processing error | Yes, soon | Retry within hours with backoff |
| Soft: generic | Do not honour | Limited | A small number of spaced retries, then ask for a new card |
| Hard | Lost/stolen, closed account, invalid number | No | Stop retries; ask the customer to update their card |
| Card expired | Expired card | No (same details) | Check for updater results; ask for new card |
| Authentication | Issuer requires SCA | No | Email a link to authenticate on-session |
Smart Retry Schedules
A naive retry schedule, say every day for a week, is the worst of both worlds. It hammers issuers on hard declines and misses the moments when soft declines are most likely to succeed.
Better schedules are built from a few principles:
- Classify first. Only retry decline codes that are genuinely retryable.
- Space retries out. A few attempts across one to three weeks tends to outperform many attempts in a few days.
- Time them sensibly. For insufficient funds, retrying just after common pay dates or at the start of the day is more likely to succeed than retrying at 2am on the same day.
- Cap attempts. Respect scheme limits and set your own lower cap.
- Keep delivering if the business model allows. For a physical subscription, decide explicitly whether a box ships while payment is in recovery. That is a commercial decision; engineering's job is to make it configurable.
Most payment platforms now offer this as a feature. Stripe has Smart Retries, Adyen has Auto Rescue, and billing platforms such as Chargebee and Recurly have their own retry logic. Whether you use the provider's engine or build your own depends on how much of your billing logic you own. If your subscription engine is in-house, you will probably need your own scheduler, informed by the provider's decline data.
Card Account Updater and Network Tokens
The cheapest failed payment to recover is the one that never fails. Card account updater services let the card schemes pass on new card details when a card is reissued or its expiry changes. Visa Account Updater and Mastercard Automatic Billing Updater are the scheme services, and most major processors expose them, either automatically or as an opt-in feature.
Network tokens go further. Instead of storing a card number, the processor stores a scheme-issued token that stays valid when the underlying card is replaced. Issuers also tend to trust network-tokenised transactions more. If your provider supports network tokens, turning them on is usually a configuration change with a meaningful effect on expiry-driven failures.
Dunning That Doesn't Feel Like Dunning
When retries and updaters are not enough, you need the customer. A good dunning sequence is short, polite and makes it trivially easy to fix the problem:
| When | Message | Call to action |
|---|---|---|
| First failure (hard decline or expiry) | "Your card needs updating" | One-click link to update card |
| First failure (authentication) | "Please confirm your payment" | Link to authenticate on-session |
| Mid-sequence | Friendly reminder, what they'll miss | Update card |
| Final | Subscription will pause | Update card, or contact us |
Two engineering details matter more than the copy. Links should use a short-lived, signed token that takes the customer straight to the update screen without a password. And the moment the card is updated, you should retry the outstanding payment immediately rather than waiting for the next scheduled attempt.
The Engineering Underneath
Idempotency everywhere
Retries are, by definition, repeated attempts. Every charge request must carry an idempotency key tied to the business event (the invoice or order), not the attempt, so that a network timeout followed by a retry can never double-charge a customer. Webhook handlers must be idempotent too, because providers deliver events at least once and sometimes out of order.
Model it as a state machine
A payment in recovery has states: pending, failed-retryable, awaiting-customer, recovered, abandoned. Make those explicit in your data model rather than inferring them from a pile of attempt rows. It makes reporting, customer service tooling and bug-fixing much easier.
type DeclineCategory = 'soft_funds' | 'soft_technical' | 'soft_generic' | 'hard' | 'auth_required';
interface RetryDecision {
retry: boolean;
delayHours?: number;
notifyCustomer: boolean;
}
const MAX_ATTEMPTS = 4;
export function decideNextStep(category: DeclineCategory, attempt: number): RetryDecision {
if (category === 'hard' || category === 'auth_required') {
return { retry: false, notifyCustomer: true };
}
if (attempt >= MAX_ATTEMPTS) {
return { retry: false, notifyCustomer: true };
}
const delayHours = category === 'soft_technical' ? 4 : 72 * attempt;
return { retry: true, delayHours, notifyCustomer: attempt === 1 };
}Queues and scheduling
As I described in my recent post on SQS, we push payment capture onto queues so that transient errors turn into retries rather than customer-facing failures. Recovery retries fit the same pattern, with one caveat: SQS delay is capped at 15 minutes, so retries measured in days need a scheduler. A database-backed "next attempt at" column swept by a scheduled job, or Amazon EventBridge Scheduler for one-off future invocations, both work well.
Observability
Treat recovery as a funnel and instrument it. The metrics I care about are:
- Failure rate on first attempt, by decline category
- Recovery rate, split by retries, updater and customer action
- Time to recover
- Dunning email click-through and card-update completion
Alert on sudden shifts in decline mix; a spike in one code is often an integration issue, not your customers' finances.
Advice by Stage
- Startup: use your payment provider's built-in retries, updater and dunning emails. Make sure you are storing decline codes. Don't build any of this yet.
- Scaleup: own the retry policy and dunning journey, especially if your billing engine is in-house. Add idempotency keys, a recovery state machine and a funnel dashboard.
- Enterprise: experiment with retry timing by segment, consider multiple acquirers for resilience, and run involuntary churn as a standing metric alongside voluntary churn.
The Takeaway
Involuntary churn is lost revenue from customers who wanted to stay. Classify declines properly, retry only what is retryable, let updaters and network tokens prevent failures in the first place, and make fixing a card a one-click job for the customer. Build it on idempotent, observable queues and it becomes one of the quietest, highest-value systems you own.
