agent dunning loop re-sent past-due emails every poll cycle: 400 emails to one customer
Stops a dunning loop from spamming one customer hundreds of times. Use when past-due emails repeat every poll cycle or a customer reports a flood. Not for designing dunning schedules or writing the email copy.
agent dunning loop re-sent past-due emails every poll cycle: 400 emails to one customer
TL;DR
Make dunning sends idempotent: record each sent reminder per invoice (invoice id + dunning stage) and skip any stage already recorded, no matter how many times the poller runs. If a flood already happened, pause the job, apologize once, and fix the guard before resuming. The poller should ask "what is newly due?" not "what is due?"
agent dunning loop re-sent past-due emails every poll cycle: 400 emails to one customerFix it
- Record every send. Before sending, write (invoice id, stage, timestamp) to durable storage; check it first and skip on a hit. Success check: running the poller twice in a row sends zero emails the second time.
- Stage the dunning, don't blast it. Define stages (day 3, day 7, day 14) and only send when the invoice crosses into a new stage. Success check: an invoice 10 days overdue has exactly 2 reminders on record.
- Add a circuit breaker. If one customer would receive more than 2 emails in 24h, halt and page a human instead. Success check: a forced flood in staging trips the breaker and sends an alert, not 400 emails.
- Recover from a flood. Stop the job, send one apology with a correct statement of what is owed, and confirm the guard is live before restarting. Success check: no customer receives more than one apology plus their normal staged reminders.
When this applies
- A cron, agent, or billing worker sends payment reminders on a schedule.
- A customer reports receiving the same past-due email repeatedly.
When it doesn't
- The reminder schedule itself is wrong (too aggressive by design); that is a policy decision, not a loop bug.
- Emails are bouncing or landing in spam; that is deliverability, not duplication.
Compatibility
- Pattern: agent-driven dunning on Stripe Billing, Chargebee, Recurly, or any system where the agent decides when to email.
- Works with any mail provider; the guard lives in your code, not the mailer.
Variant: agent re-sent "payment failed" emails on every webhook retry
Webhook retries re-fire the same event. Same fix: dedupe on event id before sending.
Variant: two agents both ran dunning
Overlapping jobs each think they own reminders. Same fix plus a single owner: one job, or a distributed lock around the send step.
Why it happens
The poller queries "all overdue invoices" every cycle and emails each one, with no memory of what it already sent. Every cycle is a fresh blast to the same customers.
Edge cases
- Invoice state changes: if an invoice is paid then goes past-due again later, allow a new dunning sequence; key the record on (invoice id, stage) and reset when status returns to paid.
- Partial payments: stage on remaining balance thresholds, not just age, so a customer who paid 90% doesn't get the day-14 tone.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_usdDUBX4e2fimkHWAHGehQ