Dunning recovery
A subscription payment fails, and most small businesses find out weeks later — once the customer has quietly churned and the revenue is gone. This is a small serverless system that catches the failed charge the moment the payment processor reports it, works through a smart retry schedule that backs off over several days and skips weekends, and sends a branded dunning email at each attempt. If every retry fails it pauses the customer’s access, and it restores access the instant a retry succeeds or the customer updates their card. It never double-charges and it never silently cancels anyone. Seven posts on the same system — one diagram at a time — with an engineering reference at the end.
-
01
A dunning recovery system on AWS for a few dollars a month
The whole system on one page — a failed-charge intake, a retry engine, and an access-and-mail piece — plus the five states every dunning subscription moves through.
-
02
How a failed charge gets caught
How a failed charge is caught the moment it happens: a signed payment-processor webhook hits a Lambda Function URL, the signature is verified, the event is de-duplicated, and the subscription enters dunning — all before any retry is scheduled.
-
03
How a retry schedule gets decided
How a retry schedule gets decided: a plain-Python backoff over several days that skips weekends, written as one-off EventBridge Scheduler rules, with a hard idempotency guard so a charge is never retried twice.
-
04
How a dunning email gets sent
How a branded dunning email gets sent at each attempt: a template from S3, one Bedrock Haiku call to write copy in your voice that escalates gently, an embedded update-card link, and SES outbound — with bounces and complaints handled.
-
05
How access gets paused and restored
How access gets paused after the final failed retry and restored the instant a payment succeeds or a card is updated: an entitlement check your app calls, a reversible paused state, and never a silent cancellation.
-
06
What dunning recovery costs
What dunning recovery costs: a line-by-line breakdown that sums to about $1.90/month at ~150 subscriptions and ~30 failed charges, dominated by Secrets Manager, plus what it looks like at ten times the volume.
-
07
Engineering reference: the dunning recovery architecture
The engineering reference: the full AWS topology, the Lambda inventory with runtimes, the EventBridge Scheduler config, the DynamoDB tables and key schemas, the S3 bucket, the SES setup, IAM scope notes, the Bedrock model id, and the region.
Frequently asked questions
- What is dunning recovery?
- Dunning is the polite, structured process of recovering a subscription payment that has failed — usually because a card expired, hit its limit, or was declined for risk. This system listens for the payment processor’s failed-charge webhook, decides a smart retry schedule (a backoff over several days that skips weekends), retries the charge at each step, and sends a branded dunning email each time. After the final failed retry it pauses the customer’s access; it restores access the moment a retry succeeds or the customer updates their card. Every attempt and state change is logged. It is designed on AWS for about $1.90/month at small-business volume.
- How much does it cost to run?
- About $1.90/month at typical small-business volume (around 150 active subscriptions and roughly 30 failed charges a month). The biggest single line is Secrets Manager — three secrets at $0.40 each — because the actual work is so cheap: the retry schedule is plain Python, Lambda and DynamoDB cost pennies, and SES outbound mail is $0.10 per thousand. At ten times the volume (around 1,500 subscriptions, 300 failed charges) it lands around $4.80, because the fixed Secrets Manager cost doesn’t grow.
- Which AWS services does it use?
- Lambda (Python 3.14, arm64) with Function URLs for the processor webhook and the entitlement check, EventBridge Scheduler for the one-off retries and a daily reconcile sweep, DynamoDB on-demand for dunning state and the audit log, S3 for email templates and the monthly report, SES (outbound only) for the branded dunning emails, SQS with a dead-letter queue to buffer webhook events, Secrets Manager for the processor API key and webhook secret, CloudWatch Logs at 7-day retention, AWS Budgets, and Bedrock (Claude Haiku 4.5 via Global cross-Region inference) only to write the email copy. No API Gateway, no NAT Gateway, no always-on compute. One region throughout: eu-west-2.
- How does the retry schedule work?
- When the processor reports a failed charge,
dunr-schedulerlays out a backoff in plain Python: a first retry the next business day, then roughly every three business days, for four retries over about ten days. Weekends are skipped, because a card that declined on Friday is no more likely to clear on Saturday, and a dunning email lands better on a weekday. Each retry is a one-off EventBridge Scheduler rule that firesdunr-retrierat the exact moment. A retry that succeeds cancels every remaining scheduled retry; a retry that fails advances to the next step, or, if it was the last one, pauses access. - Does it use AI?
- Sparingly, and never on a decision that touches money. The retry schedule, the charge retry, and the pause/restore of access are all deterministic Python. Bedrock’s Claude Haiku 4.5 is called only inside
dunr-mailer, to write the branded dunning email copy in your voice — gentle on the first nudge, clearer about the consequence by the last — from a template you control. The monthly recovery report uses one more small call. No model ever decides whether to retry, charge, pause, or restore. - Can it double-charge or cancel someone on its own?
- No, on both counts. Every retry is guarded by an idempotency key tied to the invoice and attempt number, so a duplicated EventBridge trigger or a replayed webhook can never charge a customer twice. And it never silently cancels: the worst it does on its own is pause access after the final failed retry — a reversible state — while the customer keeps getting a clear path to update their card. Cancelling a subscription outright is always a human decision, taken in your billing tool, not something this system does.