DEV Community

Your bounce handler is probably suppressing the wrong address

Here is a bug I have now seen in four separate codebases, including one I wrote.

Mail goes out. A bounce comes back. The handler parses the status code, sees a 5.x.x, concludes the address is bad, and adds the recipient to the suppression list. Clean, obvious, and wrong for an entire class of bounce.

Because not every 5.x.x is about the recipient. Some of them are about you.

Two families, one code space

Enter fullscreen mode Exit fullscreen mode

Both are permanent. Both are 550. Suppressing on the first is correct — that mailbox is not there and will not be there tomorrow.

Suppressing on the second is a mistake that compounds. 5.7.x is a policy rejection: the receiver evaluated your IP, your domain, your authentication or your reputation, and declined. The recipient is fine. Their mailbox exists. They might be your best customer.

What you have actually learned is that you cannot currently deliver to that provider. Recording it against the recipient throws away the real signal and permanently burns an address you never had a problem with.

The sub-code is the part that matters

The three-digit code tells you almost nothing. The enhanced status code after it — RFC 3463 — tells you who the problem belongs to.

Class Meaning Suppress the recipient?
5.1.x Addressing. No such user, bad syntax. Yes
5.2.1 Mailbox disabled Yes
5.2.2 Mailbox full No — this is temporary in practice, retry later
5.4.x Routing / network No
5.7.x Policy, authentication, reputation No — this is about you

5.2.2 deserves a note of its own. It is formatted as permanent and behaves as temporary, because mailboxes get emptied. Treating a full mailbox as a dead address is a slow, invisible leak from your list.

What to do with a 5.7.x instead

The useful unit is not the recipient, it is the (sending identity, receiving provider) pair.

Keep a separate ledger of policy refusals recording the sending domain or IP, the receiving provider, the code, and the raw text. Then act on aggregates rather than individual events:

  • Rising 5.7.x from one provider against one sending domain means stop sending to that provider from that domain and fix the cause.
  • The same code across every provider means the problem is the domain or the IP, not any single relationship.
  • A provider-specific code — Google's 5.7.26, for instance, which means unauthenticated — tells you precisely what is broken. 5.7.26 is an authentication failure, not a reputation verdict, and it is fixable in DNS this afternoon.

The practical shape is an automatic hold: when policy refusals from one provider cross a threshold, pause sending to that provider from that sender, and keep the recipients untouched. Delivery resumes when the cause is fixed. Nothing permanent was recorded about anyone.

Parse the enhanced code properly

The enhanced status code is not always where you expect it, and plenty of servers put a different code in the text than in the status field.

import re

ENHANCED = re.compile(r"\b([245])\.(\d{1,3})\.(\d{1,3})\b")

def classify(smtp_response: str):
    m = ENHANCED.search(smtp_response)
    if not m:
        # fall back to the basic code; 5xx without an enhanced code
        # is genuinely ambiguous — do not guess
        return "unknown"
    cls, subject, _detail = m.group(1), m.group(2), m.group(3)
    if cls != "5":
        return "transient"
    if subject == "1":
        return "bad_recipient"     # safe to suppress
    if subject == "7":
        return "policy"            # about the SENDER — never suppress
    return "other"
Enter fullscreen mode Exit fullscreen mode

Two things that save you later: search the whole response, not just the first line, because multi-line replies routinely carry the enhanced code on a continuation line. And when there is no enhanced code at all, record unknown rather than defaulting to "bad recipient" — a bare 550 from an unfamiliar server is not enough to destroy an address over.

Why this stays invisible

Nothing about this bug looks like a bug. Your bounce rate goes down after you start suppressing policy refusals, because you stop retrying addresses that were refusing you. The list gets smaller and cleaner-looking. Meanwhile the actual problem — an authentication failure, a reputation issue, a provider that has decided something about your domain — is being quietly converted into permanent suppressions and never surfaced to anyone.

If you want to know whether the cause is authentication rather than reputation, that part is checkable in about thirty seconds: SPF, DKIM and DMARC against live DNS. Authentication problems are the cheap ones. Reputation is the expensive one, and no DNS change fixes it.

Top comments (0)