Hard bounce vs soft bounce: what the SMTP codes actually mean
The receiving server tells you exactly what kind of failure you got. Most senders never read the code.
A hard bounce is a permanent rejection: the receiving server looked at the address and said it will never accept mail for it, usually with a code like 550 5.1.1. A soft bounce is a temporary failure: the same server said try again later, usually with a 4xx code like 421. Every good bounce policy follows from telling those two apart, because the correct response to one is the wrong response to the other.
Mailbox providers also read your bounce rate as a list-quality signal, and it feeds the same reputation machinery we cover in the email deliverability handbook. A sender who keeps mailing dead addresses looks like a sender who bought a list. Filters treat them accordingly.
Where a bounce actually comes from
When you send an email, your server (or your ESP's) opens an SMTP conversation with the receiving server and the receiver answers every step with a three-digit code. 2xx means accepted. 4xx means a temporary problem: stop, wait, try again. 5xx means a permanent one: stop asking.
Most rejections also carry an enhanced status code (RFC 3463), the dotted triple like 5.1.1 that rides along with the plain code. That triple is the useful part. The first digit repeats the temporary-or-permanent verdict, the second names the subsystem (1 is addressing, 2 is the mailbox, 7 is policy), and the third narrows it further. A bounce notification that only says "delivery failed" has thrown away the diagnosis; the code is the diagnosis.
The codes, and what to do with each
| Code | Class | What the server means | Treat it as |
|---|---|---|---|
| 550 5.1.1 | Permanent | This mailbox does not exist | Hard: suppress the address permanently |
| 550 5.1.2 | Permanent | The recipient's domain does not exist or has no mail host | Hard: suppress |
| 550 5.7.1 | Permanent | Rejected by policy: blocked sender, failed authentication, content | Hard code, but the cause is you, not the address. Fix your sending |
| 452 4.2.2 | Temporary | Mailbox is full | Soft: retry |
| 421 4.7.0 | Temporary | Server busy, or you are being throttled or greylisted | Soft: retry with backoff |
| 451 4.4.1 | Temporary | Connection timed out | Soft: retry |
Two of these deserve a closer look, because they are where naive bounce handling goes wrong.
550 5.7.1 is coded permanent but is not about the address at all. It shows up when authentication fails or a filter has decided it does not want your mail. Suppressing the recipient hides the real problem. If you see 5.7.1 in a bounce, go read why emails go to spam instead of blaming your list, and check that Gmail is not hard-rejecting you outright: since November 2025 both Gmail and Microsoft return 550s for unauthenticated mail rather than spam-foldering it.
Mailbox-full (452 4.2.2) is the classic gray area. The address exists, so it is not a hard bounce today. But a mailbox that stays full for two weeks belongs to someone who stopped reading email. That is exactly why a retry window needs an end date.
Our policy, as a worked example
LetterDuck is a small production sender: a live workspace since June 2026, real subscribers, low volume. The bounce policy in our send path is short enough to quote in full.
A hard bounce writes a row to the entity's suppression list the moment the bounce event arrives, and that list is checked before every single send, forever. There is no expiry and no manual review. A soft bounce gets 3 retries spread over 14 days; if the address is still failing after that, it is reclassified as hard and suppressed like one.
None of this involves a human. Every delivery, bounce, and complaint event from our relay lands in a send_event table via webhook, and suppression fires from there automatically. The send path also pauses itself if the bounce rate goes above 2% or the complaint rate reaches 0.3%. We would rather miss a send window than spend a domain's reputation finding out what was wrong.
What counts as a good bounce rate
Under 2%. That is the ceiling we enforce in code, and it is deliberately conservative.
During a new domain's ramp phase our throttle pauses sending outright at 2% bounces or 0.3% complaints, and we hold ourselves to a tighter 0.1% complaint target well before that. A genuinely opted-in list runs far below that ceiling. At our own volume, dozens of sends a day to people who asked for the mail, most weeks show zero bounces at all. That is our measurement, not an industry benchmark, and it is what small-scale opt-in looks like: bounces should be rare events with names attached, not a percentage you budget for.
If you are bouncing at 2% or more on a list you believe is clean, something specific is wrong. Old addresses that predate a sunset policy, a signup form accepting typos, or an import from a tool that never suppressed anything.
Why the 2% line exists: SES makes it concrete
Amazon SES publishes exact enforcement thresholds, and they are worth knowing even if you never touch SES, because they show how seriously infrastructure providers take bounces. SES places your whole account under review at a 5% bounce rate and pauses sending entirely at 10%. For spam complaints the review line is 0.1% and the pause line 0.5% (AWS SES documentation, checked August 2026). Not a warning email. A paused account, all recipients, all campaigns.
The gap between our 2% and their 5% is the point. Bounce rates are computed over rolling windows, so one bad segment or one stale import can triple your number in a day. Holding yourself to 2% leaves room for an accident. Running at 4% leaves none. We built automatic suppression before we were anywhere near SES scale for exactly this reason: the policy is cheap to run at 50 emails a day and very expensive to retrofit during an account review.
How to stay under it
Prevention is list hygiene, and all of it is boring on purpose.
Use double opt-in, which catches typos and bots at the door. Reject role accounts like info@, admin@, and postmaster@ at signup; they bounce, and some are spam traps. Sunset subscribers who have not engaged in 60 to 90 days. Never import a purchased list; bought addresses are where 550 5.1.1 comes from in bulk. And if the domain is new, follow a proper warm-up ramp so that one early mistake stays small.
Then watch the number. A bounce spike is usually your earliest warning that something upstream broke: a form gone bad, an import you should not have trusted, or a blacklist problem announcing itself through 5.7.1s.
Suppress every hard bounce the day it happens, cap soft-bounce retries, and treat any week above 2% as an incident. That is the whole policy, and it is enough.