Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Read and Act on Email Bounce Codes (SMTP Reference)

Every bounce carries a code that says retry, suppress or fix your setup. Here is the short table, a classifier you can run, and the free check that stops bounces before you send.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

An email bounce code is a three-part number, such as 5.1.1, that tells you whether to retry, suppress the address or fix your own sending setup. The first digit decides the first move. The second and third digits tell you what to fix.

This page gives you the codes a cold outbound pipeline meets most, a classifier that acts on them, and a free check that removes the riskiest addresses before you send. For causes, read why B2B emails bounce. For the rate to watch, read email bounce rate.

TL;DR

  • Read the enhanced status code (5.1.1), not your tool's "hard" or "soft" label. A 4 means retry, a 5 means stop and read the subject digit.
  • A 5.1.x code means the address is dead. A 5.7.x code usually means your authentication, IP or content is the problem, so the address may be fine.
  • About 30 minutes. Sizing audiences by emailStatus is free. The optional test pull spends up to 26 LeadOcean records.

Prerequisites

  • Raw bounce messages (DSNs) or the Diagnostic-Code field from your sequencer's export.
  • Python 3 for the classifier in Step 3.
  • A LeadOcean API key in $LEADOCEAN_API_KEY for Steps 4 and 5. Start free at app.leadocean.io: 1,000 records, no card.
  • curl and jq.

Step 1: Pull the code out of the bounce

Every bounce is a delivery status notification (DSN). It has a Status: field with the enhanced code and a Diagnostic-Code: field with the receiving server's own words (RFC 3464, September 2026). Keep both. The code is for your logic. The text is for the exceptions.

text
Final-Recipient: rfc822; jane.doe@acme.com
Action: failed
Status: 5.1.1
Diagnostic-Code: smtp; 550 5.1.1 The email account that you tried to reach does not exist.

The three parts are class, subject and detail. In 5.1.1, class 5 is a permanent failure, subject 1 is addressing and detail 1 is a bad destination mailbox (RFC 3463, September 2026). A 4 is a persistent transient failure: the message was valid and a later try may work.

Step 2: Decode it with the table

Use this table for the codes you will see. The first rows are on the recipient. The later rows are on you. Codes and meanings come from RFC 3463 and Google's published SMTP error list (Google Workspace Help, September 2026). The action column is our default, not a standard.

CodeMeaningWhose problemAction
5.1.1Bad destination mailboxAddressSuppress now
5.1.2Bad destination system (domain)AddressSuppress, check for a typo
5.1.3Address syntax invalidAddressFix the address or suppress
5.2.1Mailbox disabled or inactiveAddressSuppress (see the note below)
5.2.2Mailbox full, now rejectingAddressPause 30 days, retry once
4.2.2Mailbox full, temporaryRecipientRetry over 3 to 7 days, then stop
4.4.7Delivery time expiredNetworkRetry later
4.4.5Server busyReceiverRetry with backoff
4.7.0Rate limited or low reputationYouSlow down, fix reputation
5.4.4Unable to routeDomainCheck MX, suppress the domain
5.6.0Message malformedYouFix the message
5.7.1Refused by policyUsually youRead the text before suppressing
5.7.26Sender unauthenticatedYouFix SPF, DKIM or DMARC

Two rows deserve a second look. Google uses 5.2.1 for an inactive account and also for a recipient receiving mail too fast, so read the diagnostic text. And 5.7.1 covers a custom admin rule, a bad IP reputation and likely spam, none of which is a dead mailbox.

When a 5xx is not a dead address

The costly mistake is suppressing every 5xx. RFC 3463 says a permanent failure is one that will not resolve by resending the message in its current form. It does not say the address is bad. The subject digit tells you which part of the message or setup has to change.

Use the subject digit as a quick sort:

  • Subject 1 is addressing. The mailbox or domain is wrong. Suppress.
  • Subject 2 is the mailbox. It is disabled or full. Suppress or pause.
  • Subject 4 is routing and network. Check DNS and the domain's mail server.
  • Subject 6 is the message itself. Fix the content or headers.
  • Subject 7 is security or policy. Check your SPF, DKIM, DMARC, IP and links.

Each provider also publishes its own list. Google lists its codes with exact wording, and Microsoft documents its Exchange Online non-delivery reports (Microsoft Learn, September 2026). Match the text from the receiving provider against its own list before you write a rule.

A retry policy that fits on one line

Retry a 4xx three times over about three days, with longer gaps each time. After that, stop and log the address as undelivered. Never retry a 5.1.x code. Do not mix retries with new sends in the same batch, or you hide a real bounce rate behind repeat attempts.

Step 3: Classify bounces in code

Classify on the enhanced code and keep the diagnostic text for review. This script reads a CSV of bounces with a diagnostic column and writes an action for each row. It is a starting point, so tune the maps to your own policy.

python
import csv, re, sys

SUPPRESS = {"5.1.1", "5.1.2", "5.2.1", "5.1.3"}
FIX_SETUP = {"5.7.1", "5.7.26", "5.6.0", "4.7.0"}
RETRY_CLASS = "4"
CODE = re.compile(r"\b([245])\.(\d{1,3})\.(\d{1,3})\b")

def action(text):
    m = CODE.search(text)
    if not m:
        return "review", None
    code = m.group(0)
    if code in SUPPRESS:
        return "suppress", code
    if code in FIX_SETUP:
        return "fix_setup", code
    if m.group(1) == RETRY_CLASS:
        return "retry", code
    return "review", code

for row in csv.DictReader(open(sys.argv[1])):
    a, c = action(row["diagnostic"])
    print(f'{row["email"]},{c},{a}')

Run it as python3 classify.py bounces.csv > actions.csv. Rows marked fix_setup are not suppressed. Check your authentication and sending IP first, because the same fault will reject other addresses too.

Step 4: Size the risky groups before you send

The cheapest bounce is the one you never send. LeadOcean publishes email_status for every person, and the filter emailStatus takes the values from GET /v1/enums/email_status. Sizing is free: send count=true as a query parameter with limit=1 (LeadOcean OpenAPI, September 2026).

bash
for s in verified catch_all risky; do
  curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
    -H "x-api-key: $LEADOCEAN_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"emailStatus\": [\"$s\"], \"jobLevel\": [\"VP\"], \"country\": [\"US\"]}" \
    | jq -c "{status: \"$s\", total: .meta.total}"
done

Passing emailStatus replaces the default filter, which counts verified, catch_all_valid and catch_all. So you choose the group. Send verified first. Send catch_all later in a smaller batch. Leave risky, role, invalid, disposable, spam_trap and abuse out.

Step 5: Confirm one address before a big pull

Search rows carry email_status, but the docs warn that a search count of verified is an upper bound while the stricter rule rolls out. Enrichment applies the narrow rule per address, so it is the check to trust. Pull a small page, then enrich one person by person_id.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "Content-Type: application/json" \
  -d '{"emailStatus": ["verified"], "jobLevel": ["VP"], "country": ["US"], "limit": 25}' \
  | jq -r '.meta.nextCursor, .data[0].meta.source_ids.person_id'

curl -s -X POST "https://api.leadocean.io/v1/people/enrich" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "Content-Type: application/json" \
  -d '{"person_id": "1234567890", "reveal_email": true}' \
  | jq '.contact_data.contact_emails'

Replace 1234567890 with the id the first call prints. The search spends 25 records. The enrichment spends one. LeadOcean does not refund or credit back bounced emails, so read the status before you send.

What you get

Free counts from the LeadOcean MCP tool leadocean_count_leads on 2026-10-21, for people who are VPs in the US, split by emailStatus only. These are counts by email status. They are not mailable counts. The REST loop in Step 4 stops at 100,000 per call, so use the MCP tool or narrower filters for buckets this wide.

emailStatus filterPeopleMeaning
verified257,840Upper bound while the stricter rule rolls out
catch_all984,961Also matches catch_all_valid
risky72,112Use with care

The enrichment call returns the shape below. This is the response shape from the API reference, with placeholder values.

json
{
  "contact_data": {
    "has_email": true,
    "email_status": "verified",
    "email_type": "work",
    "contact_emails": [
      {
        "email": "jane.doe@acme.com",
        "type": "work",
        "status": "verified",
        "verified_batch_date": "2026-09-20"
      }
    ]
  }
}

A null in verified_batch_date means our own verifier never tested that address, per the API reference. Treat those as lower confidence than a dated verified.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key headerSend x-api-key: $LEADOCEAN_API_KEY and check the key in the app
429Above your plan's rate limit per account (5 a second on Free, 200 on Agency)Wait the Retry-After seconds, add a short sleep or a queue, then retry
402The key has spent its recordsCheck usage in the app. Free is 1,000 one-off records
Empty resultsA filter value that is not an exact enum stringCheck the value against GET /v1/enums/email_status and the other enums
404 on enrichWe hold nothing for that personNothing to fix. A 404 costs no record
Export refusedOver 50,000 rows a request or 500,000 a daySplit the export into smaller filtered runs

FAQ

What is the difference between a hard and a soft bounce code?

A 5xx is a permanent failure and a 4xx is a persistent transient failure. The label your tool shows is a summary of that first digit. Read the full code, because 5.7.x often blames your setup, not the address.

Does a 550 always mean suppress the address?

No. A 550 5.1.1 means the mailbox does not exist, so suppress it. A 550 5.7.1 is a policy refusal and can come from your IP, content or a recipient admin rule. Read the text first.

How many times should I retry a 4xx bounce?

Retry with growing gaps for a few days, then stop. The 3 to 7 day window in the table is our default, not a standard. Keep a count per address so a stuck mailbox does not retry forever.

Can catch all addresses bounce?

Yes. A catch all domain says yes to every address at send time, so a missing mailbox can bounce later. See catch all emails explained. We have not run a bounce test, so this page gives no rate.

Where do I go next?

Start with bounce checker to email bounce management. Plans are Free, Solo at $97, Agency at $497 and Platform at $1,997 a month, on the pricing page. More guides are in the blog hub.

Size your verified and catch all lists before you send

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →