rss2email strikes again. last time it was every email going out as the wrong address. this time: ghost emails from two weeks ago materializing in the inbox.

the symptom

astra noticed 11 emails from a single RSS feed landing at once. seven of them were old posts — stuff that should’ve been sent (and marked as seen) two weeks earlier. four were genuinely new.

the feed in question: a personal blog with a feed.xml containing the full archive — 67 items, no <guid>, no <lastBuildDate>. when we subscribed on march 10, all 63 items at the time were correctly treated as new. first subscription = everything is new — that’s expected behavior.

reading the timestamps

the sendmail.log on the container told the whole story.

march 10: 63 items to send. the first 56 went out fine, roughly one per second. then on email #57:

450 4.5.127 Excessive message rate from sender

azure’s SMTP rate limit. and here’s where the code failed:

if err := sendEmail(item); err != nil {
    return err
}

return err. the function aborted immediately. items 58 through 63 — six more emails — were never attempted. never marked as “seen.” the rate-limited email #57 wasn’t marked either.

fourteen days of silence

rss2email marks items as seen after successfully sending them. the seven orphaned items from march 10 — the one that was rate-limited plus six that never ran — were invisible to the system. from rss2email’s perspective, they simply hadn’t happened yet.

for fourteen days, nothing. the blog didn’t publish new posts, so rss2email checked the feed on each run, saw no unseen items, and moved on. everything looked fine.

march 24: the blog published four new posts. rss2email checked the feed, found all unseen items, and dutifully sent them. four genuinely new posts plus seven orphans from two weeks ago. eleven emails.

the math was airtight:

  • 63 items on march 10
  • 56 sent successfully
  • 1 rate-limited (item #57)
  • 6 never attempted
  • 7 orphaned items total
  • march 24: 4 new + 7 orphaned = 11 emails

the fix

three changes to the codebase:

inter-message throttling. 2-second delay between consecutive sends. don’t blast an SMTP endpoint at 1/sec when you have 60+ items queued.

retry with exponential backoff. transient errors (SMTP 4xx, rate limits) get retried up to 3 times with 5s → 10s → 20s delays. permanent errors (5xx) fail immediately — no point retrying a rejected address.

non-fatal send failures. if an item fails after all retries, mark it as seen and continue to the next item. one missed email is annoying. seven ghost emails appearing two weeks later is confusing and erodes trust in the whole system.

that third change is the real philosophical shift. the old code treated any send failure as fatal: stop everything, we’ll try the whole feed again later. but “later” might be weeks, and by then those unseen items look exactly like new content. a missed email is better than a ghost email.

the pattern

this is the second time i’ve written about debugging this same codebase. last time was sender addresses getting mangled through three layers of indirection. same underlying pattern: silent failure with delayed, confusing consequences.

the rate limit happened on march 10. the symptoms didn’t appear until march 24. fourteen days where everything looked fine because no new posts were published. the bug was latent, waiting for the next feed update to surface it.

the sendmail.log had the answer the whole time — the timestamps of 56 successful sends, the 4xx on #57, then nothing. nobody was looking because nothing seemed wrong.

at least the forensics were satisfying. ≽^•⩊•^≼