An SMTP bounce code is the three-digit reply a receiving mail server gives when it refuses a message, usually followed by a dotted enhanced status code that says why. 550 5.1.1 means the mailbox does not exist and is a hard bounce; 452 4.2.2 means the mailbox is full and is a soft bounce; 421 4.7.0 means the server is throttling you and says nothing about the mailbox at all. Reading the second number is what turns a bounce log into a decision.
How are SMTP reply codes structured?
RFC 5321 defines the three-digit reply. The first digit is the verdict, the second the category, the third a detail.
| First digit | Meaning | What a sender does |
|---|---|---|
| 2xx | Success | Continue |
| 3xx | More input needed (only 354 after DATA) |
Send the content |
| 4xx | Transient failure | Queue and retry later |
| 5xx | Permanent failure | Stop and bounce |
The second digit narrows it: x0x syntax, x1x informational, x2x connection, x5x mail system. In practice the second digit of the plain code is coarse, which is why almost every modern server appends an enhanced status code.
What are enhanced status codes?
RFC 3463 adds a class.subject.detail triple after the reply code, and it is the part worth parsing.
| Class | Meaning |
|---|---|
| 2.x.x | Success |
| 4.x.x | Persistent transient failure: retrying may succeed |
| 5.x.x | Permanent failure: retrying will not help |
| Subject | Area |
|---|---|
| x.0.x | Other or undefined |
| x.1.x | Addressing: the recipient or sender address |
| x.2.x | Mailbox: full, disabled, over quota |
| x.3.x | Mail system: the receiving system itself |
| x.4.x | Network and routing: cannot reach the next hop |
| x.5.x | Mail delivery protocol: bad commands or sequence |
| x.6.x | Message content or media |
| x.7.x | Security or policy: authentication, spam, rate limits |
A 5.1.1 is therefore a permanent addressing failure, and 4.7.1 a transient policy failure. The subject tells you whether the problem is the address, the mailbox, the network or your own reputation, and that is the axis a list-hygiene decision turns on.
Which codes will you actually see?
| Reply | Meaning | Bounce type | What it says about the address |
|---|---|---|---|
250 2.1.5 |
Recipient accepted | none | The server will take mail for it; on a catch-all server that is all it proves |
550 5.1.1 |
Bad destination mailbox address, "user unknown" | hard | The mailbox does not exist. Remove the address |
550 5.1.2 |
Bad destination system address | hard | The domain part is wrong or unroutable |
550 5.1.3 |
Bad destination mailbox syntax | hard | The address itself is malformed |
550 5.2.1 |
Mailbox disabled, not accepting messages | hard | The account exists but is closed or suspended |
552 5.2.2 |
Mailbox full, permanent | hard | Over quota and not expected to clear; treat as risky at best |
452 4.2.2 |
Mailbox full, transient | soft | Over quota right now; retry in days |
450 4.2.1 |
Mailbox temporarily unavailable | soft | Often a hold or migration; retry |
450 4.7.1 or 451 4.7.1 |
Try again later, policy | soft | Greylisting: the server wants a retry from the same sender |
421 4.7.0 |
Too many connections or messages | soft | Throttling of the sender; nothing about the address |
421 4.3.2 |
System not accepting network messages | soft | The server is shutting the session; retry later |
451 4.4.1 or 4.4.2 |
Connection or transmission failure | soft | Network trouble on the receiving side |
550 5.7.1 |
Delivery not authorized, message refused | hard for this message | A policy rejection: blocklist, reputation, missing authentication. The mailbox may be fine |
550 5.7.26 |
Unauthenticated email is not accepted | hard for this message | The sender failed SPF or DKIM alignment; fix authentication, not the list |
Two rules of thumb follow. A 5.1.x or 5.2.1 reply is about the address and justifies removing it. A 5.7.x reply is about you, the sender, and removing the address would hide a problem you need to fix.
What is the difference between a hard and a soft bounce?
A hard bounce is a permanent 5xx failure about the recipient: the mailbox or domain does not exist. Sending platforms suppress hard-bounced addresses automatically and mailbox providers count repeated hard bounces against your reputation. A soft bounce is a transient 4xx failure: the mailbox is full, the server is busy, the message is too large. Senders retry soft bounces for a day or two and then give up.
The line blurs at the edges. A 552 5.2.2 is permanent by class but describes a full mailbox, and a 550 5.7.1 is permanent for this message but says nothing about the recipient. Classifying purely by the first digit gets both wrong, which is why the enhanced code matters.
How does a verification handshake use these codes?
A verifier never sends DATA, so it only sees replies up to RCPT TO. That is enough to classify the mailbox, and Spaw reports the classification in smtp_reason next to the verdict.
| Server reply during the handshake | smtp_reason |
Verdict |
|---|---|---|
5.1.1 user unknown at RCPT TO |
no_mailbox |
undeliverable, reason mailbox_not_found |
5.2.1 mailbox disabled |
disabled |
undeliverable, reason mailbox_disabled |
5.2.2 or 4.2.2 mailbox full |
mailbox_full |
risky, reason mailbox_full |
4.7.x come back later |
greylisted |
rests on the other signals, smtp_checked: false |
| No reply within the time limit | timeout |
rests on the other signals |
| Connection refused on port 25 | no_connect |
rests on the other signals |
| Host unreachable | mx_unreachable |
rests on the other signals |
5.7.x policy rejection or any ambiguous refusal |
unverified, never guessed | rests on the other signals |
Only an explicit "no such user" becomes no_mailbox. A policy rejection during verification is not evidence about the mailbox, so Spaw leaves mailbox_exists at null rather than turning a reputation problem on the verifier's side into a bounce prediction for you.
curl https://spaw.co/api/v1/email \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'
{
"success": true,
"data": {
"email": "[email protected]",
"deliverable": "undeliverable",
"reason": "mailbox_not_found",
"risk_score": 100,
"smtp_checked": true,
"mailbox_exists": false,
"smtp_reason": "no_mailbox",
"// 20 more fields": "see the endpoint reference"
},
"meta": { "credits_used": 0, "credits_remaining": 10, "cache_hit": false }
}
Undeliverable answers are always free, and the address is added to your suppression list so batch and bulk runs answer it without another handshake.
What should you do with bounce codes from your own sends?
Feed them back. The codes your sending platform records after a real delivery attempt are stronger evidence than any handshake, in both directions: a delivery proves the mailbox, a 5.1.1 disproves it. Post the outcome to the feedback endpoint with the diagnostic string, or point your provider's bounce webhook at Spaw and let the adapters for Postmark, Amazon SES, Mailgun and SendGrid map their event types. Hard bounces join your suppression list, deliveries turn later lookups into confirmed mailboxes, and the per-domain bounce rate feeds the confidence score for catch-all addresses.
What to do next
- Map your bounce log against the
smtp_reasonvalues on the mailbox-check details page. - Verify addresses before sending with the handshake described here; the test address
[email protected]shows the exact shape of a hard-bounce answer. - Send your provider's bounce events to the feedback endpoint so
5.1.1replies from real sends suppress addresses automatically. - Treat every
5.7.xreply as a sender problem and check your SPF, DKIM and DMARC before touching the list.