Skip to content

Read a bounce: eight classes, and what each one should do to your list

Paste a real diagnostic and see how the product classifies it, including how long a soft bounce suppresses. Runs the actual classifier in your browser.

DeliverabilityIntermediate11 min readUpdated

01Paste a bounce and see the classification

This widget imports classifyBounce and softSuppressionDays from @mailysend/events/bounce — the same two functions the event consumer calls. Paste a diagnostic you actually received and the answer here is the answer your instance would record.

TL;DR

The classifier reads four inputs in a fixed order of preference, and the enhanced status code inside the diagnostic is worth more than everything after it combined.

BOUNCE CLASSIFIER

Runs classifyBounce and softSuppressionDays from @mailysend/events. This is the answer your instance would record.

hard_invalid
The address does not exist. Suppress it permanently.
Permanent — the address is suppressed and will not be retried.
1
Provider verdict
providerType / providerSubType
2
Enhanced code
RFC 3463, from the diagnostic text
3
Phrase match
six lists, in order
4
SMTP leading digit
5 → hard_invalid, 4 → soft_temporary
5
unknown
no suppression

Give it the full line where you have it, including the SMTP code and the enhanced status code. Each rung is only reached because the one above it found nothing, so a diagnostic that carries a code never touches the phrase lists at all.

WHY THIS RUNS IN YOUR BROWSER
The widget imports the classifier from its subpath export rather than the package barrel, because the barrel re-exports the queue consumer, which reaches into platform and database code. Classifying a string should not drag a database client into a marketing page — and running the real function is the only way this page can promise that what you read is what you get.

02The distinction that actually matters

Eight classes exist so you can reason about causes, but only one distinction changes what happens to the address: hard suppresses permanently, soft holds for a window and then releases. Everything expensive about bounce handling comes from getting that binary wrong, and it is expensive in both directions.

TL;DR

Hard suppresses forever, soft parks the address for a bounded number of days — and the two ways of being wrong about that cost you completely different things.

Suppressing too eagerly
  • You lose a real subscriber, forever, silently.
  • Suppress on a full mailbox and you have permanently removed somebody who opted in, reads your mail, and whose only offence was being over quota for a week
  • Nothing ever tells you: no error, no complaint, no event — the address simply stops appearing in sends
  • Your list quietly shrinks by however many people were behind on their inbox that Tuesday
Not suppressing at all
  • You keep hammering a receiver that is grading you on it.
  • Send again to an address that does not exist and the receiver records another invalid-recipient attempt
  • That rate is one of the clearest signals a large receiver has for telling a maintained list from a scraped one
  • You are not just failing to reach one person — you are paying for it with the deliverability of everyone else on the list

These two costs are not symmetrical in shape. The first is a slow leak you cannot observe; the second is a compounding penalty applied to your entire audience. That is why the taxonomy is conservative in one specific place — an unrecognised diagnostic does not suppress — and aggressive everywhere it is certain. Certainty is the thing being paid for, and it is why enhanced status codes matter so much.

03The eight classes, and why each exists

Each class exists because there is an action attached to it that the others do not share. Where two classes would lead to the same decision every time, they were not split. Where a single label would hide a decision, it was.

TL;DR

Three hard classes that never release, four soft classes with windows of one to seven days, and one deliberate unknown that does nothing at all.

ClassEffectHolds forMeans
hard_invalidPermanent · suppressnull — never releasedThe mailbox does not exist at that domain.
hard_domainPermanent · suppressnull — never releasedThe receiving domain does not resolve or has no route for mail.
hard_blockedPermanent · suppressnull — never releasedYou were refused by policy. The address is probably real.
soft_mailbox_fullTemporary · suppress7 daysA real person with a full mailbox.
soft_throttledTemporary · suppress1 dayThe receiver is rate-limiting you, or greylisting.
soft_contentTemporary · suppress3 daysRefused on content or authentication grounds.
soft_temporaryTemporary · suppress2 daysA generic 4.x.x failure with nothing more specific to say.
unknownNo suppression at allnull — nothing to releaseNothing in the diagnostic was recognised.

The Holds for column is softSuppressionDays(class) verbatim — a null means the suppression never ends for a hard class, and that there is no suppression at all for unknown.

1 · hard_invalid

This is the only class that is unambiguously about the address itself. There is no future in which sending again works, and every repeat attempt is a documented invalid-recipient hit against your reputation at that receiver.

2 · hard_domain

Separated from invalid because the failure is one level up: a typo in the domain part, an expired registration, a company that no longer exists. Nothing about the local part is being asserted, and if the domain ever comes back the address may be fine.

3 · hard_blocked

The most important distinction in the taxonomy. It suppresses, because continuing to send is the worst thing you can do — but it is evidence about you, not about the recipient. A hundred of these to one provider is a reputation incident, not a hundred bad addresses, and a UI that files them next to typos will let you miss that.

4 · soft_mailbox_full

The single most expensive class to get wrong. Whoever this is opted in, still exists, and will empty their mailbox eventually — or will not, in which case they stop opening and your engagement segments will retire them for you. Suppressing permanently here throws away a subscriber on the strength of a temporary condition.

5 · soft_throttled

Not a failure at all in the usual sense: it is the receiver telling you the pace is wrong. The correct response is to slow down, which is why the send path halves its learned quota on rejection rather than retrying harder into the same wall.

6 · soft_content

Classified soft on purpose. The message is the problem, not the address — fix the authentication or the content and the same recipient accepts your next send. Treating it as a hard bounce would permanently suppress people because of a broken DKIM key.

7 · soft_temporary

The honest catch-all for a temporary code the classifier can read but cannot narrow. Two days is short enough that a real outage resolves inside the window and long enough that you are not hammering a struggling server.

8 · unknown

Deliberately does nothing. An unrecognised diagnostic is a gap in the classifier, not evidence about the recipient, and the cost of guessing wrong is a real subscriber lost permanently. The address stays sendable and the raw diagnostic stays stored so the gap can be closed.

One input sits above all of this. Some providers pre-classify a bounce for you, and that classification is trusted where present — they can see things this side cannot: their own suppression list, their feedback loops, the history of that address across every sender they carry. The subtype is still read alongside it, so a permanent verdict whose subtype mentions a domain failure lands in hard_domain, one mentioning suppression lands in hard_blocked, and a transient one mentioning a full mailbox lands in soft_mailbox_full rather than the generic buckets.

04How long a soft bounce holds

A soft bounce does not suppress an address. It parks it. The window is chosen per class from how long the underlying condition plausibly lasts — long enough that retrying is not futile, short enough that a recovered mailbox rejoins the next send.

TL;DR

One day for throttling, two for a generic temporary failure, three for content, seven for a full mailbox — and no window at all for anything else.

ClassHoldWhy that long
soft_throttled1 dayRate limits and greylisting resolve in minutes to hours. A day is already generous; anything longer punishes you for the receiver’s pacing.
soft_temporary2 daysA generic 4.x.x failure is usually an outage or a queue problem. Two days clears almost all of them without a week of silence.
soft_content3 daysLong enough that you have to notice and actually fix something — a DKIM key, a link domain, a subject line — before the address is retried.
soft_mailbox_full7 daysThe longest window, because a full mailbox is the slowest human condition here and the most costly to give up on. A week is roughly the gap to your next send.
everything elsenullHard classes are already permanent, and unknown is deliberately not suppressed at all — there is no window to compute.
softSuppressionDays(bounceClass)

  soft_mailbox_full  → 7
  soft_throttled     → 1
  soft_content       → 3
  soft_temporary     → 2
  default            → null   ← hard classes and unknown
null on a hard class
  • There is no window because the suppression never ends
  • The address is out of every future send until you act on it deliberately
null on unknown
  • There is no window because there is no suppression
  • The address stays sendable and the raw diagnostic stays stored
  • If you build reporting on this, do not collapse the two into “not suppressed for a while”

05Enhanced status codes beat text every time

RFC 3463 defines a three-part status code — class.subject.detail — that sits alongside the three-digit SMTP reply. Where it is present it is the single best signal available, because it is a machine-readable claim by the receiver about what went wrong, rather than a sentence written for a human by whoever configured the MTA.

TL;DR

The class digit decides permanence and the subject.detail pair decides the class, so the same subject can land hard or soft depending only on whether it starts with a 5 or a 4.

550 5.1.1 <[email protected]>: Recipient address rejected
 │   │ │ │
 │   │ │ └── detail   the specific condition
 │   │ └──── subject  what the code is about (addressing, mailbox, policy…)
 │   └────── class    2 success · 4 temporary · 5 permanent
 └────────── SMTP reply code, much coarser
CodeClassReading
5.1.1 · 5.1.3 · 5.1.6hard_invalidBad destination mailbox address.
4.1.1 · 4.1.3 · 4.1.6soft_temporaryThe same subjects with a temporary class digit — the addressing claim is not trusted as permanent.
x.1.2hard_domainBad destination system address. Permanent whichever class digit carries it.
x.2.2soft_mailbox_fullMailbox full — never permanent, whatever the class digit says.
5.7.xhard_blockedSecurity or policy refusal, permanent.
4.7.xsoft_throttledThe same refusal, temporarily — usually pacing or greylisting.
5.3.4 · x.2.3soft_contentMessage too big for the system, or over the mailbox’s message limit.
x.4.xsoft_temporaryNetwork and routing status, always read as temporary.
anything elsehard_invalid / soft_temporaryAn unmatched code still tells you its class digit: 5 is read as invalid, 4 as temporary.

Read straight off the enhanced-code branch of classifyBounce. Only the diagnostic text is searched for a code — the SMTP reply field alone never produces one.

Plenty of MTAs never emit one. Older Exim and Postfix configurations, appliances in front of corporate mail, and anything hand-rolled will hand you 550 No such user here and nothing else. For those, the classifier falls back to six phrase lists, tried in this order against the diagnostic, the provider type and the provider subtype joined together and lowercased:

Falls back toIf the text contains any of
soft_mailbox_fullmailbox full · mailbox is full · over quota · quota exceeded · insufficient storage
soft_throttledrate limit · too many · throttl · try again later · deferred · greylist
soft_contentspam · content rejected · message rejected for policy · dmarc · spf · dkim
hard_invalidno such user · no such mailbox · user unknown · recipient rejected · recipient address rejected · does not exist · invalid recipient · unknown user
hard_domaindomain not found · no mx · host unknown · nxdomain · unrouteable
hard_blockedblocked · blacklist · blocklist · denied · reputation · spamhaus

Order matters: a diagnostic saying “message deferred, mailbox full” matches the first list and is classified soft_mailbox_full, not soft_throttled.

An enhanced code
  • A machine-readable claim the receiver deliberately published
  • Stable across MTAs and independent of language
  • Wins over the phrase lists every time both are present
A phrase match
  • A list of things people have actually seen, in English
  • No guarantee the next MTA phrases it the same way
  • Good enough to be useful, not good enough to be trusted over a code

What just happened

You can now look at a diagnostic string and say which of the eight classes it lands in, whether it suppresses, and for how long. The two things most likely to bite you later are both misreadings of a class rather than a bug: a run of hard_blocked is a reputation incident being filed as a list-hygiene problem, and an unknown bounce is an unanswered question rather than a clean delivery. Watch the volume of both, not just the totals.

Common questions

Read next

YOUR ACCOUNT, YOUR MAIL

Nothing to sign up for. Just deploy it.

Every guide on this site describes software you run yourself.