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.
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.
Runs classifyBounce and softSuppressionDays from @mailysend/events. This is the answer your instance would record.
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.
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.
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.
- 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
- 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.
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.
| Class | Effect | Holds for | Means |
|---|---|---|---|
| hard_invalid | Permanent · suppress | null — never released | The mailbox does not exist at that domain. |
| hard_domain | Permanent · suppress | null — never released | The receiving domain does not resolve or has no route for mail. |
| hard_blocked | Permanent · suppress | null — never released | You were refused by policy. The address is probably real. |
| soft_mailbox_full | Temporary · suppress | 7 days | A real person with a full mailbox. |
| soft_throttled | Temporary · suppress | 1 day | The receiver is rate-limiting you, or greylisting. |
| soft_content | Temporary · suppress | 3 days | Refused on content or authentication grounds. |
| soft_temporary | Temporary · suppress | 2 days | A generic 4.x.x failure with nothing more specific to say. |
| unknown | No suppression at all | null — nothing to release | Nothing 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
| Class | Hold | Why that long |
|---|---|---|
| soft_throttled | 1 day | Rate limits and greylisting resolve in minutes to hours. A day is already generous; anything longer punishes you for the receiver’s pacing. |
| soft_temporary | 2 days | A 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_content | 3 days | Long 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_full | 7 days | The 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 else | null | Hard 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- There is no window because the suppression never ends
- The address is out of every future send until you act on it deliberately
- 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.
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
| Code | Class | Reading |
|---|---|---|
| 5.1.1 · 5.1.3 · 5.1.6 | hard_invalid | Bad destination mailbox address. |
| 4.1.1 · 4.1.3 · 4.1.6 | soft_temporary | The same subjects with a temporary class digit — the addressing claim is not trusted as permanent. |
| x.1.2 | hard_domain | Bad destination system address. Permanent whichever class digit carries it. |
| x.2.2 | soft_mailbox_full | Mailbox full — never permanent, whatever the class digit says. |
| 5.7.x | hard_blocked | Security or policy refusal, permanent. |
| 4.7.x | soft_throttled | The same refusal, temporarily — usually pacing or greylisting. |
| 5.3.4 · x.2.3 | soft_content | Message too big for the system, or over the mailbox’s message limit. |
| x.4.x | soft_temporary | Network and routing status, always read as temporary. |
| anything else | hard_invalid / soft_temporary | An 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 to | If the text contains any of |
|---|---|
| soft_mailbox_full | mailbox full · mailbox is full · over quota · quota exceeded · insufficient storage |
| soft_throttled | rate limit · too many · throttl · try again later · deferred · greylist |
| soft_content | spam · content rejected · message rejected for policy · dmarc · spf · dkim |
| hard_invalid | no such user · no such mailbox · user unknown · recipient rejected · recipient address rejected · does not exist · invalid recipient · unknown user |
| hard_domain | domain not found · no mx · host unknown · nxdomain · unrouteable |
| hard_blocked | blocked · 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.
- 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 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.