Key takeaways
- A hard bounce means the address will never work. Sending to it again is a reputation signal, not just a wasted message.
- The hard/soft binary is a simplification. Read the enhanced status code —
5.2.2(mailbox full) is technically permanent but behaves transiently. - Complaints matter more than bounces. Keep the rate under 0.10% and never let it reach 0.30%.
- Suppression must be enforced at send time, not as a nightly cleanup job. One retry loop against a bad address undoes the whole system.
- Spam traps produce no bounce and no complaint. The only defence is never emailing addresses you did not collect and confirm yourself.
- Your hygiene protects your domain reputation. The IP reputation you are also judged on is shared with every other sender on the same addresses.
Every mail system produces failures. Addresses get deleted, mailboxes fill up, domains expire, and receiving servers throttle. Handling those failures correctly is unglamorous work that turns out to be one of the highest-leverage things you can do for deliverability, because the receiving side reads your failure rate as a proxy for how carefully you manage your recipients.
The reasoning is straightforward from their perspective. A sender who knows their audience sends almost entirely to real, active mailboxes. A sender who repeatedly attempts delivery to addresses that have not existed for two years is either careless or working from a list they did not collect. Neither is a sender worth giving the benefit of the doubt.
This guide covers how to classify failures, what to do with each class, and how to build the loop so it maintains itself.
Hard bounces, soft bounces, and why the binary is imprecise
The conventional split is between permanent failures (hard) and temporary ones (soft). It maps onto the SMTP reply code classes: 5xx is permanent, 4xx is transient. That distinction is real and useful, but treating it as the whole story causes two specific mistakes — suppressing addresses that were only temporarily unavailable, and retrying addresses that will never work.
| Type | SMTP class | Meaning | Correct action |
|---|---|---|---|
| Hard bounce | 5xx | The address or domain does not exist, or delivery is permanently refused | Suppress immediately and permanently |
| Soft bounce | 4xx | Temporarily undeliverable — mailbox full, server down, greylisted, rate limited | Retry with backoff; suppress only after repeated failures |
| Policy block | 5xx, often 5.7.x | The receiver refused on reputation, authentication, or content grounds | Do not suppress the recipient — fix the underlying cause |
| Complaint | No SMTP failure | Delivered, then the recipient reported it as spam | Suppress immediately; investigate the stream |
The most useful precision comes from the enhanced status code defined by RFC 3463 — the X.Y.Z triplet that accompanies the reply. Its first digit repeats the class, the second identifies the subject (addressing, mailbox, mail system, network, protocol, content, or policy), and the third gives detail. Classifying on the triplet rather than the bare reply code is what lets you distinguish "no such user" from "mailbox full" from "we do not like your reputation."
A working SMTP code reference
| Code | Meaning | Class | Action |
|---|---|---|---|
5.1.1 | Bad destination mailbox — no such user | Hard | Suppress permanently |
5.1.2 | Bad destination system — domain does not resolve | Hard | Suppress permanently |
5.1.10 | Recipient address rejected — commonly a null MX | Hard | Suppress permanently |
5.2.1 | Mailbox disabled — exists but not accepting mail | Hard | Suppress; may be reactivated later |
5.2.2 | Mailbox full | Permanent class, transient reality | Retry for a limited window, then suppress |
4.2.2 | Mailbox full (transient form) | Soft | Retry with backoff |
4.7.x / 421 | Rate limited, greylisted, or temporarily deferred on policy | Soft | Back off; persistent occurrence means a reputation problem |
5.7.1 | Delivery not authorised — policy rejection | Policy | Investigate reputation or content; do not suppress the recipient |
5.7.26 | Gmail: message not authenticated | Policy | Fix SPF, DKIM, and DMARC alignment |
5.7.515 | Microsoft: sending domain fails required authentication level | Policy | Publish and align SPF, DKIM, and DMARC |
5.2.2 is the classic edge case: the code class says permanent, but mailboxes get emptied. Retry it for a bounded window rather than suppressing on first sight.Complaints: the signal that matters most
A complaint is a recipient pressing "report spam" on a message that was successfully delivered. It is the most direct negative signal in email, because it is an explicit human judgment rather than an inferred one, and it is weighted accordingly.
Complaints reach you through feedback loops, where a participating provider forwards a notification when one of their users reports your mail. Coverage is uneven — some large providers participate, others expose complaint data only through their postmaster dashboards — so your observed complaint rate is a floor rather than a complete count.
Target rate
< 0.10%
Google’s recommendation
Hard ceiling
0.30%
throttling begins
Healthy transactional
< 0.01%
requested mail rarely gets reported
Denominator
Per 1,000
0.3% is 3 complaints per 1,000
Two operational points follow. First, complaints must trigger immediate suppression — a recipient who reported you once and receives another message will report again, and the second complaint costs more than the first. Second, complaint rate must be tracked per stream. A blended figure across receipts and campaigns hides exactly the problem you need to see, which is one of the arguments for separating the streams entirely.
Transactional mail generating meaningful complaint volume is itself a finding. It usually means one of three things: the messages are not genuinely transactional, the from domain is not recognisable to recipients, or people are receiving mail for accounts they did not create — which points at a signup flow without email confirmation.
Spam traps and why they are invisible
A spam trap is an address that exists solely to catch senders who should not be mailing it. It accepts mail, so there is no bounce. Nobody reads it, so there is no complaint. The only evidence you ever get is a reputation that degrades without an obvious cause.
| Type | Origin | How you hit it |
|---|---|---|
| Pristine trap | Never belonged to a person; published only where scrapers find it | Purchased lists, scraped addresses |
| Recycled trap | A real address, abandoned, then repurposed by the provider after a dormancy period | Mailing addresses that have not engaged in years |
| Typo trap | A common misspelling of a large domain, registered deliberately | Signup forms with no validation or confirmation step |
The defences are entirely preventative. Never send to an address you did not collect yourself. Confirm addresses at signup rather than accepting whatever was typed. Suppress hard bounces immediately, since a recycled trap is usually preceded by a period of bouncing before the provider reactivates it. And remove long-dormant addresses from any non-transactional stream, because dormancy is exactly the condition that precedes recycling.
The handling architecture
A correct implementation has one property above all: suppression is enforced at send time, by the system that sends. A nightly reconciliation job is not sufficient, because a retry loop firing at 3am against a bad address will do the damage before the job runs.
- 1
Subscribe to bounce and complaint webhooks
At minimum
email.bounced,email.complained, andemail.failed. Verify the signature on every payload before acting on it. - 2
Classify on the enhanced status code
Branch on the
X.Y.Ztriplet rather than the bare reply code, so mailbox-full is not treated the same way as no-such-user. - 3
Write to the suppression list
Hard bounces and complaints suppress immediately. Soft bounces increment a counter and suppress only after repeated failures across separate sends.
- 4
Enforce suppression before the message is composed
The send path checks the list and refuses. This is the step that makes the rest work — everything else is bookkeeping if a retry can bypass it.
- 5
Reflect it in your own product
Mark the user record so your application knows the address is unreachable, and surface it in the UI so a human can correct it. A suppression the customer never learns about becomes a silent support problem.
- 6
Alert on the rates, not the events
Individual bounces are noise. A hard bounce rate crossing 2% in an hour, or a complaint rate crossing 0.1% in a day, is a signal worth waking someone for.
Handling a bounce webhook
app.post('/webhooks/supersendtx', async (req, res) => {
if (!verifySignature(req)) return res.sendStatus(401)
const { type, data } = req.body
if (type === 'email.bounced' || type === 'email.complained') {
await db.user.update({
where: { email: data.to[0] },
data: { emailReachable: false, emailFailureReason: data.bounce_reason },
})
}
// Suppression itself is enforced provider-side: subsequent sends to a
// suppressed recipient are refused with 422 before composition.
res.sendStatus(200)
})- · Respond 2xx quickly and do the work asynchronously — webhook endpoints that block get retried.
- · Handle events idempotently; at-least-once delivery means duplicates.
On SuperSend TX, hard bounces, permanent SMTP failures, and complaint events add to your account’s suppression list automatically, and POST /emails checks every recipient before sending — a suppressed address returns 422 with the offending recipients listed, and emits an email.suppressed event so the attempt is visible rather than silently dropped. Managed unsubscribe links write to the same list. You can list, add, and remove entries through the suppressions API.
Preventing bounces at the source
Everything above is remediation. The cheaper intervention is upstream, in the form that collects the address in the first place.
- Validate syntax and MX at signup. A domain with no MX record cannot receive mail; rejecting it in the form costs nothing and prevents a guaranteed bounce.
- Catch common typos.
gmial.com,hotmial.com, andyaho.comare typo-trap territory. Suggest the correction inline rather than accepting it. - Confirm the address before sending anything else. A confirmation step is the single most effective control: it proves the mailbox exists, proves the person controls it, and prevents someone signing another person up.
- Do not accept role addresses where they do not belong.
info@,admin@, andpostmaster@are frequently shared and disproportionately generate complaints. - Never import a list you did not collect. Purchased and scraped lists are where pristine traps live, and one send is enough to cause lasting damage.
- Syntax and MX validation on every address collection form
- Typo detection for the major consumer domains
- Confirmed opt-in before any non-essential mail
- Hard bounces and complaints suppressed within seconds, automatically
- Suppression enforced in the send path, not in a nightly job
- Alerting on bounce rate above 2% hourly and complaint rate above 0.1% daily
Clean lists deserve clean infrastructure
Suppression is enforced in the send path, so a retry loop cannot undo it — and the IPs underneath carry paying transactional customers only, or are yours alone on Dedicated, built and warmed by us.
Related reading