Bounce Categorization: Beyond Hard vs Soft (ESP Schemes and RFC 3463 Mapping)

Bounce categorization guide going beyond hard vs soft. Covers the five practical categories (address, mailbox, policy, content, technical) plus complaint, how to derive them from RFC 3463 enhanced status codes, ESP normalization schemes for SendGrid, Postmark, SES, and Mailgun, a working classifier in Python, cross-provider normalization patterns, edge cases (greylisting, silent drops, auto-responders), and the 10 mistakes that corrupt bounce automation. bounce-categorization-beyond-hard-vs-soft
Alaa
By Alaa
SMTPedia documents email infrastructure end to end: SMTP standards from the RFC archive, delivera...
14 min read Updated Jul 12, 2026 45 views

Bounce handling series. This is the tactical guide to categorizing bounces beyond the hard/soft dichotomy. For related guides on DSN parsing, feedback loops, and suppression lists, see the bounce handling hub →

Quick categorization reference

Hard vs soft is the entry-level classification. In production, you need more granularity: is this bounce a bad address, a full mailbox, a policy rejection, a content filter, or a technical routing issue? Each category triggers a different action (auto-suppress vs retry vs investigate) and every serious ESP normalizes bounces into 4-8 categories to make automation possible.

Underlying dataRFC 3463 enhanced status codes (X.Y.Z) inside DSNs
Practical categoriesAddress · Mailbox · Policy · Content · Technical · Complaint
Provider schemesSendGrid: 4 types · Postmark: 12 types · SES: 6 types · Mailgun: 5 types
Action per categorySuppress immediately, retry, investigate, or apply a policy fix
Common pitfallTreating policy failures (5.7.x) like bad addresses (5.1.1) and corrupting the suppression list

Every bounce your system receives eventually needs a category label. That label decides what happens next: automatic suppression, a retry queue, an alert for the deliverability team, or a policy review. Classifying every bounce as either “hard” or “soft” was enough in 2005; in 2026, with Gmail and Yahoo enforcing complaint rate ceilings and reputation systems weighting policy failures differently from mailbox failures, you need more precision. This guide covers the five practical categories that map to actions, how every major ESP normalizes their own scheme, and how to build a classifier that handles the raw data yourself.

Why hard vs soft is not enough

The hard vs soft distinction, covered in our bounce fundamentals guide, splits bounces by permanence: permanent failures (5.x.x codes) versus transient failures (4.x.x codes). This is useful, but too coarse for automation:

  • A 5.1.1 bounce (bad recipient address) and a 5.7.1 bounce (policy rejection) are both permanent, but they mean completely different things. Suppressing on 5.7.1 corrupts your list; not suppressing on 5.1.1 keeps sending to a dead address.
  • A 4.2.2 bounce (mailbox full, transient) and a 4.7.1 bounce (policy failure, transient) are both retry-able, but the underlying issue is different: one is on the recipient side, the other on your side.
  • A complaint (delivered but marked as spam by the recipient) is not a bounce at all in the traditional sense, but it needs the same suppression treatment.

The right level of granularity, for actionable automation, is five categories plus a complaint pseudo-category. The rest of this guide walks each one.

The five categories that map to actions

1. Address failure

What it is: The recipient address does not exist. The mailbox was never created, or it was deleted and the domain is now rejecting mail for it.

Enhanced codes: 5.1.1 (user unknown), 5.1.2 (bad domain), 5.1.10 (recipient address rejected).

Action: Immediate suppression. Do not retry, do not send again.

2. Mailbox failure

What it is: The address exists but the mailbox cannot accept mail right now: full, disabled, over quota, out of service.

Enhanced codes: 4.2.2 / 5.2.2 (mailbox full), 4.2.1 / 5.2.1 (mailbox disabled), 5.2.3 (message too large).

Action: Retry per schedule (usually 3 to 5 days). If persistent past a week, treat as a de facto hard bounce and suppress.

3. Policy failure

What it is: The receiver’s policy rejected the message. Could be authentication failure (SPF, DKIM, DMARC), IP or domain reputation, blocklist listing, or explicit sender block.

Enhanced codes: 5.7.1 (delivery not authorized), 5.7.26 (multiple auth failures), 4.7.0 (temporary auth failure), 5.7.606-608 (Microsoft-specific IP block).

Action: Investigate the sending setup. Do NOT suppress the recipient. Verify SPF, DKIM, DMARC (see our Authentication-Results guide). Check IP and domain reputation. Confirm no blocklist listings.

4. Content failure

What it is: The message was rejected because of its content: attachment type banned, keyword flagged, spam filter score too high, link count exceeded.

Enhanced codes: 5.6.0 (message content problem), 5.7.9 (message content not supported by policy), 5.6.11 (message content violates policy).

Action: Review the content. Do NOT suppress the recipient. Check for the flagged element (URL, attachment, phrasing). Consider A/B testing sender variants or content variants to isolate the trigger.

5. Technical failure

What it is: The bounce is not the recipient’s fault or the content’s fault; it is a routing, DNS, or infrastructure issue.

Enhanced codes: 4.4.1 (no answer from host), 5.4.4 (unable to route), 4.4.7 (delivery time expired), 5.4.6 (routing loop).

Action: Investigate. Check MX records for the recipient domain, verify DNS resolution, look for network issues. Retry once resolved.

Bonus category: Complaint (from FBL, not a DSN)

What it is: The recipient marked your message as spam in their mail client. Delivered successfully, then flagged. Reported via a Feedback Loop (FBL), not a DSN.

Format: ARF (Abuse Reporting Format, RFC 5965), sent to your registered FBL address.

Action: Immediate suppression. Log for reputation monitoring. If complaint rate exceeds 0.1 percent, investigate content, targeting, or acquisition source. FBL coverage details are in the upcoming feedback loops guide.

How ESPs normalize bounces

Every major ESP normalizes raw bounces into its own scheme, exposed via webhook events. The schemes differ, but they all map to the underlying enhanced status codes. Understanding the mapping lets you build cross-provider dashboards and switch ESPs without rebuilding your automation.

SendGrid (4 types)

TypeUnderlying codesPractical meaning
bounce5.x.x permanentHard bounce, suppress
blocked5.7.x policySending IP or domain blocked
droppedSuppressed pre-sendAddress was already on suppression
deferred4.x.x transientSoft bounce, retry queue

Postmark (12 types)

Postmark is the most granular: HardBounce, SoftBounce, Transient, Unsubscribe, Subscribe, AutoResponder, AddressChange, DnsError, SpamNotification, OpenRelayTest, Unknown, SoftBounce. The SpamNotification is Postmark’s FBL integration. DnsError maps to our Technical category. SoftBounce subdivides mailbox full vs transient network issues.

Amazon SES (6 types)

SES typePractical category
Permanent - GeneralInvestigate; unclear failure
Permanent - NoEmailAddress
Permanent - SuppressedAlready suppressed on SES account level
Transient - GeneralMailbox or Technical
Transient - MailboxFullMailbox
Transient - MessageTooLargeContent

Mailgun (5 types)

hard_bouncePermanent failure, address usually
soft_bounceTransient, mailbox usually
suppress-bounceAlready suppressed pre-send
suppress-unsubscribeUnsubscribed
suppress-complaintFBL complaint

The lesson: none of these schemes is granular enough to derive category directly. You need to look at the actual SMTP response or the underlying enhanced status code to distinguish an address failure from a policy failure. Both look like hard_bounce in Mailgun, both look like bounce in SendGrid, both need very different handling.

Building a classifier from raw data

The goal: given a raw SMTP response or DSN, output one of the five categories (or complaint if the input is an FBL ARF report). A decision tree:

Input: enhanced_status (X.Y.Z), smtp_reply (3-digit), diagnostic_text

Step 1: Is this an FBL ARF report?
  Yes -> Category: COMPLAINT
  No  -> continue

Step 2: What is the X.Y.Z class?
  2.x.x -> Success, not a bounce
  4.x.x -> continue with transient logic
  5.x.x -> continue with permanent logic

Step 3: What is the Y (subject)?
  X.1.z  -> ADDRESS failure
  X.2.z  -> MAILBOX failure
  X.3.z  -> TECHNICAL failure (system full)
  X.4.z  -> TECHNICAL failure (routing)
  X.5.z  -> TECHNICAL failure (protocol)
  X.6.z  -> CONTENT failure
  X.7.z  -> POLICY failure

Step 4: For 4.x.x, action is always RETRY
         For 5.x.x, action is:
           ADDRESS  -> SUPPRESS immediately
           MAILBOX  -> SUPPRESS if persistent (5+ occurrences)
           POLICY   -> INVESTIGATE (sender-side issue)
           CONTENT  -> INVESTIGATE (content issue)
           TECHNICAL -> INVESTIGATE (routing issue)

A minimal Python implementation:

CATEGORY_ADDRESS = 'address'
CATEGORY_MAILBOX = 'mailbox'
CATEGORY_POLICY = 'policy'
CATEGORY_CONTENT = 'content'
CATEGORY_TECHNICAL = 'technical'

SUBJECT_MAP = {
    '1': CATEGORY_ADDRESS,
    '2': CATEGORY_MAILBOX,
    '3': CATEGORY_TECHNICAL,
    '4': CATEGORY_TECHNICAL,
    '5': CATEGORY_TECHNICAL,
    '6': CATEGORY_CONTENT,
    '7': CATEGORY_POLICY,
}

def categorize(enhanced_status):
    if not enhanced_status or '.' not in enhanced_status:
        return 'unknown', 'investigate'
    parts = enhanced_status.split('.')
    if len(parts) != 3:
        return 'unknown', 'investigate'
    cls, subj, detail = parts
    category = SUBJECT_MAP.get(subj, 'unknown')
    if cls == '4':
        action = 'retry'
    elif cls == '5':
        if category == CATEGORY_ADDRESS:
            action = 'suppress'
        elif category == CATEGORY_MAILBOX:
            action = 'suppress_if_persistent'
        else:
            action = 'investigate'
    else:
        action = 'noop'
    return category, action

This handles the standard DSN flow. For raw SMTP responses without enhanced codes, you fall back to matching the diagnostic text against known patterns. The SMTP error codes reference covers the common patterns per provider.

Cross-provider normalization in practice

If you use multiple ESPs (transactional through one, marketing through another) or if you might switch providers, build an internal normalization layer that maps every provider’s scheme to your five categories. Store bounces with both the raw provider event and the normalized category. This gives you:

  • A single dashboard across providers.
  • Consistent automation logic regardless of which ESP delivered.
  • Portability if you switch ESPs; your automation does not need rewriting.

Example normalization for SendGrid webhook events:

def normalize_sendgrid(event):
    event_type = event.get('event')
    smtp_reply = event.get('reason', '')
    status = event.get('status', '')  # e.g. "5.1.1"

    if event_type == 'bounce':
        # Use the enhanced status if present
        if status:
            return categorize(status)
        # Fall back to reason text matching
        if 'user unknown' in smtp_reply.lower():
            return CATEGORY_ADDRESS, 'suppress'
        if 'mailbox full' in smtp_reply.lower():
            return CATEGORY_MAILBOX, 'retry'
        return 'unknown', 'investigate'

    if event_type == 'blocked':
        return CATEGORY_POLICY, 'investigate'

    if event_type == 'deferred':
        return CATEGORY_MAILBOX, 'retry'  # or Technical, needs subject
        # Best practice: parse the smtp_reply for the enhanced code

    if event_type == 'spamreport':
        return CATEGORY_COMPLAINT, 'suppress'

    return 'unknown', 'noop'

Repeat per provider. The normalization function is straightforward once you understand each provider’s event schema.

Edge cases every classifier misses at first

Reputation-based silent drops

Some receivers, notably Microsoft Outlook.com, silently drop mail without generating a DSN when reputation is very poor. You never see a bounce; you just see delivered messages that never appear in the recipient’s inbox. Diagnostic: check Google Postmaster Tools and Microsoft SNDS for reputation data separately.

Greylisting looks like 4.7.1

Greylisting is a temporary policy rejection: the receiver rejects the first delivery attempt for an unknown sender and asks you to retry in 5 to 15 minutes. Its enhanced code is often 4.7.1, which our classifier would put in the Policy category, but the action is a normal retry, not an investigation. Well-behaved sending MTAs handle greylisting automatically; your application-level classifier can ignore 4.7.1 for the first 24 hours.

Auto-responders masquerading as bounces

Out-of-office replies sometimes look structurally similar to DSNs. Detect them by the Auto-Submitted: header (RFC 3834) which auto-responders should set. Do not suppress or categorize auto-responder replies.

Delayed bounces (async DSN, hours or days later)

A DSN generated by an intermediate relay may arrive days after the original send, after the receiver’s final rejection. Correlate via Message-ID, do not assume same-day arrival. Some MTAs retry for 5 days before generating the final DSN.

10 common bounce categorization mistakes

  1. Suppressing on any 5.x.x code. Only address failures (5.1.x) and persistent mailbox failures should trigger suppression. Policy failures indicate a sending-side problem.
  2. Not distinguishing content from policy failures. Content rejections (5.6.x) may not repeat if you change the message; policy rejections (5.7.x) need infrastructure investigation.
  3. Treating greylisting like a real policy failure. Greylisting is designed to be transparent to a well-behaved sender. Wait 24 hours before investigating 4.7.1 codes.
  4. Ignoring auto-responders as bounces. Detect the Auto-Submitted: header and skip them.
  5. Not correlating with the original send Message-ID. Without correlation, category patterns cannot be tied to specific campaigns, templates, or segments.
  6. Applying uniform retry schedule to all 4.x.x codes. A 4.2.2 (mailbox full) may resolve in hours; a 4.7.1 (policy) needs a longer retry window.
  7. Not normalizing across providers. Sooner or later you will add a second ESP. Store both raw and normalized.
  8. Trusting the ESP category without checking the SMTP code. “Blocked” in one ESP can mean either policy or reputation. Look at the underlying diagnostic.
  9. Ignoring FBL complaints. Complaints are technically not bounces but need the same suppression treatment plus reputation monitoring. Feedback loops are covered in the next guide of this series.
  10. Not building a “reason unknown” bucket. Some bounces do not fit clean categories. Give them a “review manually” bucket instead of forcing them into a wrong category.

Bounce categorization FAQ

Why do ESPs use different bounce categorization schemes?

The underlying data (RFC 3463 enhanced status codes) is standardized, but each ESP chose its own way to expose it. SendGrid picked 4 broad types, Postmark picked 12 granular types, SES picked 6 medium types. The differences reflect product decisions: how much granularity to expose to customers, how easy to make the webhook events for beginners, how to match the SMTP data to their internal reputation system. All schemes can be traced back to the same underlying RFC 3463 codes if the raw bounce data is preserved.

Should I automatically suppress on every hard bounce?

Only for address failures (5.1.x codes). Suppressing on 5.7.x (policy) means removing legitimate recipients from your list because your own SPF is broken. Suppressing on 5.6.x (content) means removing recipients because one specific message hit a spam filter. The differentiation happens through categorization: address failures suppress immediately, everything else goes through a review process.

What about “unknown” or “other” categories?

Every classifier has an “unknown” bucket. Some bounces have malformed status codes, some are free-text rejections with no structure, some come from custom anti-spam systems with proprietary error formats. Route these to a manual review queue rather than defaulting to suppression. Over time, the patterns you see in “unknown” tell you what heuristics to add to the classifier.

How do I categorize a bounce without an enhanced status code?

Fall back to matching the diagnostic text against known patterns. “User unknown” and “mailbox unavailable” indicate address failures. “Mailbox full” or “over quota” is mailbox. “Message content rejected” or “content violation” is content. “Sender IP blocked” or “reputation” is policy. Match case-insensitively and keep a list of patterns per provider. The SMTP error codes reference documents common patterns per provider.

Does bounce categorization matter if I use an ESP that handles suppression for me?

Yes, for two reasons. First, most ESPs suppress on any hard bounce, which includes policy failures where the problem is on your side. Understanding categorization lets you unsuppress or investigate. Second, if you switch ESPs or add a second one, your automation logic needs consistent categorization to work across providers. Even when the ESP handles suppression, categorization is what tells you WHY messages are failing at a rate that hurts deliverability.

How often should I audit my bounce categorization logic?

Review the categorization outcomes quarterly. Look for patterns: is the “unknown” bucket growing (add heuristics)? Are policy failures suddenly spiking on one segment (investigate infrastructure)? Are mailbox failures concentrated on old imports (list hygiene)? Bounce categorization is not a set-and-forget system; the patterns evolve as your traffic mix changes and as receivers update their reject reasons.

Final words

Bounce categorization is the layer that turns raw bounce data into automated decisions. Every hard bounce is not a hard bounce in the sense that matters for your suppression list. Every soft bounce is not a mailbox issue. Getting the five categories right (address, mailbox, policy, content, technical) plus the complaint pseudo-category, and mapping every provider scheme to them, is what makes bounce handling operationally sane.

The mental model to keep: the second digit of the enhanced status code (X.Y.Z, the Y) is the category signal, the first digit (X) is the retry signal, the third (Z) is context. Once your classifier reliably extracts Y, most automation writes itself.

For the full picture, see our guides on DSN parsing (the raw data source), hard bounce vs soft bounce (the underlying dichotomy), the SMTP error codes reference, and the Authentication-Results header for the 5.7.x policy failure diagnostic. The next article in this series covers feedback loops (FBL) and the complaint category in detail.

Address-category bounces are the easiest to prevent.

SMTPing catches invalid addresses, disposables, catch-all traps, role accounts and dead mailboxes at collection time. Fewer 5.1.1 bounces means less classifier work and a cleaner suppression list. 13 validation types, 25 free daily.

Try SMTPing →

About the Author

Alaa - SMTPedia author

Alaa · LinkedIn

Email infrastructure specialist with 8+ years of hands-on experience in SMTP, deliverability, and email verification. I’ve configured and troubleshot mail systems across Postfix, Exchange, and cloud relays, managed IP reputation and warmup campaigns, and built verification pipelines processing millions of addresses. My work spans DNS authentication (SPF, DKIM, DMARC, BIMI), bounce handling, blocklist monitoring, and compliance frameworks including CAN-SPAM and GDPR. I write every article on SMTPedia to give email professionals, developers, and marketers the accurate, RFC-grounded reference they need.


About SMTPedia

SMTPedia is an independent email industry reference covering SMTP, IMAP, POP3, email deliverability, marketing platforms, DNS authentication, and email verification. Every article is researched from official provider documentation, IETF RFCs, and industry best practices. Settings and configurations are verified quarterly.

We are cited as a source by ChatGPT, Microsoft Copilot, and thousands of email professionals worldwide. Learn more about our editorial process.