When a client tells me “we have a deliverability problem,” I rarely start with subject lines, design, or segmentation. I go straight to the SMTP logs, and especially the return codes. It’s simple, direct, and precise.
Codes tell you who said no (you, a relay, or the recipient domain), why (address, policy, size, pacing, reputation), and how severe (temporary vs. permanent). In other words, they turn “my email didn’t arrive” into decisions: retry, slow down, clean, fix infra, or suppress.
This is the most technical post in my bounce series. I won’t cover business impact (reputation, revenue, blocklists) or the “root causes of bounces”, those are in other parts. Here we go backend: how codes are structured, how to scan them quickly, common traps, and how I normalize everything in my playbooks.
Foundations: where codes come from and how to read them
1. Two places you’ll see codes
During the SMTP dialog (synchronous).
Your server (or ESP) talks to the recipient’s server: HELO/EHLO → MAIL FROM → RCPT TO → DATA
. At each step, the far end replies with a 3-digit code (plus text). If it refuses, you’ll see a 4xx
(temporary) or 5xx
(permanent) right there.
Later, in a DSN (bounce message).
Sometimes the message is accepted, then bounces downstream (intermediate relay, internal rule). You get a non-delivery email with Enhanced Status Codes like 5.1.1
, 4.2.2
, etc. That’s the X.Y.Z
format (details below).
Why it matters: who’s speaking changes how reliable the diagnosis is. A 550
during RCPT TO (“user unknown”) is near-certain. A 5.2.2
(“mailbox full”) inside a DSN might reflect a local policy, so it’s a bit more interpretive.
2. The classic 3-digit SMTP codes
First digit = severity
2xx
success3xx
“continue” (e.g., 354 “Start mail input”)4xx
temporary failure (soft) → retry5xx
permanent failure (hard) → do not retry
Second digit = broad family
You’ll see x0x
(syntax), x1x
(addresses/info), x2
x (connections), x3x
(mail system), x4x
(network). In modern ops, focus on the first digit and the common specifics below.
Frequent examples
421
Service not available (down/maintenance) → soft450
Mailbox unavailable (temporary) → soft451
Local error / temporary policy → soft452
Insufficient system storage → soft550
Mailbox unavailable / user unknown → hard551
User not local → usually hard (unless clear forward-path)552
Exceeded storage allocation → soft or hard (policy-dependent)553
Mailbox name not allowed → mostly hard554
Transaction failed / policy violation → often hard
3. Enhanced Status Codes X.Y.Z
These show up in DSNs and add precision:
- X = class:
2
success,4
temporary failure,5
permanent failure. - Y = subject:
1
address,2
mailbox,3
mail system,4
network/routing,5
protocol,6
content/media,7
security/policy. - Z = detail: the exact reason (
.1
“doesn’t exist,”.2
“disabled,”.4
“too big,” etc.).
High-value examples (you’ll see these a lot)
5.1.1
: recipient doesn’t exist → hard5.1.10
: addressing/routing problem → usually hard5.2.1
: mailbox disabled → hard4.2.2
: mailbox full → soft5.3.4
: message too large → often hard (varies by domain)4.4.1
: connection timed out → soft4.4.2
: bad connection / network → soft5.4.1
: routing failure → hard4.7.0
: policy/temp (greylisting, throttling) → soft5.7.1
: policy/security refusal (auth, spam) → hard5.7.26
: DMARC non-alignment / unauthenticated sender → hard (until fixed)
Shortcut: if you see 4.x.x
→ soft, 5.x.x
→ hard (with a few nuances like 5.2.2
/ 5.3.4
depending on the provider). I’ll show my “cautious override” logic below.
My quick-read bounce checklist
- Check the class first:
4xx
/4.x.x
(soft) vs5xx
/5.x.x
(hard). - Identify the subject (the Y in
X.Y.Z
):.1
address,.2
mailbox,.3
system,.4
network,.5
protocol,.6
content,.7
security/policy. - Default actions:
5.1.1
,5.2.1
→ immediate suppress (hard).
4.2.2
,4.4.1
,4.7.0
→ backoff retries (soft). - Read the human text: many domains add clear hints (“mailbox full,” “unauthenticated due to DMARC”). Great for edge cases.
- Normalize the reason (my taxonomy):
user_unknown
,mailbox_full
,message_too_large
,throttled
,policy_block
,auth_missing
, etc. - Decide exceptions (rare, documented): e.g.,
5.3.4
“too big”, I lighten and run a single post-fix test if the text suggests a flexible policy.
Codes you’ll see most, and what to do
Grouped by operational intent (how you act), not by RFC. That’s how I run it day-to-day.
1. Addressing & existence
550
/ 5.1.1
: user unknown
Hard. Suppress immediately and trace the source (collection, import, partner).
Do next: stronger form validation (syntax + MX), double opt-in, real-time verification.
5.1.0
/ 5.1.3
: invalid address (syntax)
Hard. Should’ve been blocked at intake.
Do next: fix front-end + server-side checks.
551
: user not local
Mostly hard. Exception only if the text shows a valid forward-path.
Do next: if no routing info, suppress.
553
: mailbox name not allowed
Hard. Often illegal characters or a role-based address blocked by policy.
Do next: clear role-based policy + filter upstream.
2. Mailbox & quota
4.2.2
: mailbox full
Soft. Backoff retries (3–5 attempts), then freeze if recurring.
Gotcha: some domains return 5.2.2 (permanent) for long-over-quota boxes. I default to soft unless the text says “persistent/long overdue,” then I flip to hard after 2 campaigns.
5.2.1
: mailbox disabled
Hard. Suppress; audit the source.
3. Message size & content
5.3.4
: message too big
Often hard (strict policy). I fix size and run a contained test, no mass retries.
Do next: compress images, remove attachments, host media, minify HTML.
4.5.3
: too many recipients / distribution limits
Soft.
Do next: split sends, respect limits.
4. Connection, network, pacing
421
/ 4.4.1
/ 4.4.2
: unavailable, timeout, network issues
Soft. Recipient is down or you’re pushing too fast.
Do next: backoff, per-domain pacing, wider send windows, watch DNS/TLS.
4.7.0
: throttling, greylisting, “try again later”
Soft. The “come back politely” signal.
Do next: slow for that domain, add spaced retries (15 min → 1h → 4h → 12h). With decent reputation, success is the norm.
5. Policy, security, authentication
5.7.1
: delivery not authorized / policy
Hard. Policy/security refusal (sender, risky content, blocked domain).
Do next: verify SPF/DKIM/DMARC, avoid sketchy shorteners, simplify tracking/redirects, check the reputation of linked domains (LP/CDN).
5.7.26
: DMARC/SPF/DKIM misaligned (unauthenticated sender)
Hard until fixed.
Do next: align sending domain and signatures (SPF includes your ESP, DKIM on, DMARC aligned). Tech details live in the infra post; staying high-level here.
454
/ 5.7.0
: STARTTLS/Auth temporarily unavailable
Soft if 454, hard if persistent 5.7.x.
Do next: monitor TLS, verify certs, retry then correct.
6. Routing & DNS
5.4.1
/ 5.4.4
: routing failure / unable to route
Hard (often recipient misconfig or your identity viewed poorly).
Do next: test DNS resolution for the recipient domain; verify your HELO/PTR. Don’t mass-retry.
4.4.6
: routing loop detected
Soft in theory, but it’s a real infra issue (on their side).
Do next: limited retries + logs, then abandon if persistent.
Field notes (real patterns)
The “soft” that’s actually “hard.”
A B2B domain returned 4.2.2
mailbox full for six weeks. After two unchanged campaigns, I sunset those addresses. Result: lower pressure and better domain-level reputation.
When 5.3.4
isn’t final.
A launch newsletter full of images hit 5.3.4
at a strict provider. After minification + hosting guides, a 1,000-address test to the same domain delivered. I re-opened the segment, but only one post-fix attempt.
The 5.7.26
that explained everything.
Campaign OK everywhere, KO at a major webmail: 5.7.26
. Diagnosis: an extra ESP used for part of the flow wasn’t authorized in SPF. Fix → policy bounces gone.
My normalization method
Providers label things differently: “user unknown,” “no such user,” “unknown recipient,” etc. I map everything to a single grammar so I can have one rule per reason, not fifty synonyms.
Example mapping
Category | Code | Label | Type |
---|---|---|---|
Addressing | 5.1.1 | user_unknown | hard |
Addressing | 5.1.3 | 553 | address_syntax_invalid | hard |
Quota | 4.2.2 | 5.2.2 | mailbox_full | soft by default; hard if multi-campaign persistent |
Content | 5.3.4 | message_too_large | hard by default; single post-fix test |
Network | 4.4.1 | 421 | connection_timed_out | soft |
Network | 4.7.0 | throttled | soft + per-domain pacing |
Security | 5.7.1 | policy_block | hard |
Security | 5.7.26 | auth_failed | hard |
Tip: implement the mapping before automating decisions, or you’ll chase synonyms forever.
The automated decisions I deploy everywhere
Immediate classification
5.x.x
→ hard → instant suppress/quarantine, except reasons explicitly flagged “testable after fix” (size, specific policy).4.x.x
→ soft → retries (3–5 with backoff: 15 min → 1h → 4h → 12h → 24h).
Cautious overrides
5.2.2
mailbox full: treat as soft if text says temporary; hard if “persistent/long overdue.”5.3.4
: default hard, but allow one post-fix test (no flooding).
Cutoffs
- An address with softs across 3 consecutive campaigns → sunset.
- A domain piling up throttled → reduce pacing for that domain (not globally).
Scope
- Decide at address and domain levels. Many teams act only per address and miss the domain-level lever (crucial for throttling).
Common traps I still see too often
- Believing the human text over the code. Some servers write vague messages (“policy issue”) next to a
5.1.1
(address). Trust the code first; use the text for nuance. - Treating every
5.2.2
as hard. You’ll delete temporarily full boxes. Keep a grace window (next campaign) unless the text says “persistent.” - Retrying a
5.1.1
. Don’t. It’s dead. Retrying only hurts your reputation. - Slowing globally instead of per domain. Don’t punish the whole list for one provider’s behavior. Per-domain pacing is a force multiplier.
- Ignoring acquisition source. A spike in
5.1.1
? Check the partner or form feed for that segment before blaming “the list.”
The 250 “exception” that proves the rule
“My email was accepted (250)… and I still got a bounce!” It happens. Call it backscatter or a deferred refusal.
- Your message was accepted by a gateway (
250 OK
). - Later in the chain (internal MTA, filter, MDA), it was refused (quota, policy, recipient actually unknown).
- You receive a DSN with an Enhanced Code (often
5.x.x
).
Do next: treat the DSN like any normal bounce. 5.1.1
→ suppress. 4.7.0
→ retry with pacing. And log that the initial acceptance wasn’t the final verdict.
Pocket cheat sheet
- Invalid / doesn’t exist:
5.1.1
→ hard - Mailbox disabled:
5.2.1
→ hard - Mailbox full:
4.2.2
(sometimes5.2.2
) → soft (then sunset if it persists) - Message too large:
5.3.4
→ hard (one post-fix test, small cell) - Timeout / network:
4.4.1
,4.4.2
→ soft (retries + monitoring) - Throttling / greylisting:
4.7.0
→ soft (per-domain pacing) - Policy block:
5.7.1
→ hard (fix auth/URLs/landing) - DMARC/SPF/DKIM fail:
5.7.26
→ hard (align and re-send)
How I industrialize code reading (without losing my mind)
- Always capture (code, text) pairs per attempt (not just the last one).
- Map into 15–25 internal reasons (see above).
- Apply codified rules (hard/soft, retries, overrides, exceptions).
- Break KPIs out by domain and by acquisition source (both views are essential).
- Loop insights back to the team: collection (if
5.1.1
rises), design (if5.3.4
appears), ops (if4.4.1
spikes), deliverability (if5.7.1
/5.7.26
show up).
Less debate, more action, and bounces become predictable and easier to remove.
FAQs I get all the time
“We see 5.7.1
but the content is super clean, could it be something else?”
Yes: authentication (SPF/DKIM/DMARC), poor-reputation shorteners, mismatched sending domain, or noisy tracking (too many redirects).
“We’re stuck on 4.7.0
at one mailbox provider, now what?”
Slow down for that domain, spread the send, then ramp back up as softs drop.
“5.2.2
on a highly engaged contact from last week, possible?”
Yes (B2C inbox full). Treat as soft short-term. If it persists across 2–3 campaigns, sunset.
“Can we ignore Enhanced Codes and just keep the 3-digit ones?”
You can, but you lose precision (address vs. mailbox vs. content vs. policy). For smart automation, read both.
Where BounceStrike helps
Parsing these codes manually every day is a slog. That’s exactly why BounceStrike exists:
- Automatic decoder for SMTP/DSN replies (3-digit + Enhanced Codes) → outputs a normalized reason(
user_unknown
,mailbox_full
,throttled
,policy_block
,auth_failed
, etc.). - Ready-to-use rules (hard/soft, retries, cautious overrides for
5.2.2
/5.3.4
) you can tune to your risk tolerance. - Per-domain alerts: if
4.7.0
climbs at one provider, we tell you to slow there, not everywhere. - Clean API to connect your ESP/CRM, no more juggling 50 synonyms for “user unknown.”
Hook BounceStrike to a sample of your sends so your team knows exactly what to do in each case. The goal isn’t to rewrite your whole email strategy, it’s to bolt on a bumper that turns cryptic logs into the right moves.
Codes are your best ally, once you tame them
A bounce without a code is a guessing game. A bounce with a well-read code is a plan. Remember the simple flow:
(1) class (4 = soft, 5 = hard) → (2) subject (address, mailbox, network, content, policy) → (3) rule (retry, slim, slow, fix auth, suppress) → (4) normalize so you can automate without pain.
Day to day, this lets me:
- kill no-hope hards immediately,
- save what deserves a second try (softs),
- diagnose structural issues (collection, pacing, content, infra), and
- speak one language across teams (marketing, product, ops).
Codes aren’t here to overwhelm you, they’re here to make you fast and precise. And if you want the shortcut, BounceStrike can be your automatic bounce translator so everyone on your team knows what to do the moment a 4.7.0
or 5.1.1
shows up.