Bounces decoded: hard, soft, block, and how to handle each
What every SMTP bounce code actually means, which ones to retry, and the suppression rules that keep your reputation intact.
A bounce is a delivery failure with a story. Treat them all the same — retry forever, never suppress — and your reputation collapses in weeks. Read them properly, and they're the single best signal you have about list health and infrastructure problems.
The two-tier code system
Every bounce comes with two codes: the basic SMTP reply (5xx or 4xx) and an enhanced status code (5.1.1, 4.7.1, etc., from RFC 3463). The SMTP code tells you whether to retry. The enhanced code tells you why.
| Class | Meaning |
|---|---|
| 2.x.x | Success |
| 4.x.x | Persistent transient failure (retry) |
| 5.x.x | Permanent failure (suppress) |
Hard bounces (5xx) — suppress immediately
| Code | Meaning | Action |
|---|---|---|
| 550 5.1.1 | Mailbox does not exist | Suppress permanently |
| 550 5.1.10 | Recipient address rejected | Suppress |
| 550 5.4.1 | Recipient address rejected — relay denied | Likely typo |
| 552 5.2.2 | Mailbox full | Soft-treat: retry once, then suppress if persists |
| 553 5.1.3 | Bad destination address syntax | Suppress |
Soft bounces (4xx) — retry with backoff
| Code | Meaning | Strategy |
|---|---|---|
| 421 4.7.0 | Service temporarily unavailable | Standard retry |
| 450 4.2.1 | Mailbox temporarily unavailable | Standard retry |
| 451 4.7.1 | Greylisted | Wait 5–15 min, retry |
| 452 4.2.2 | Mailbox full | Retry slowly, suppress after 5 days |
| 421 4.7.0 | Try again later (rate limit) | Honor backoff signal |
Standard retry strategy: 4 attempts over 72 hours with exponential backoff (15min, 1h, 6h, 24h). After that, suppress. Most ESPs do this automatically; if you're hand-rolling, set a time-based ceiling and stop.
Block bounces — the dangerous middle ground
Block bounces look like 5xx but indicate reputation problems, not bad addresses. They mean the receiver is rejecting your IP or domain, not the recipient. Suppressing the address won't help; you have to fix reputation.
| Code | Meaning | Real fix |
|---|---|---|
| 550 5.7.1 | Message blocked due to policy | Check authentication, content, IP reputation |
| 550 5.7.26 | DMARC policy not aligned | Fix DMARC alignment |
| 554 5.7.1 | Spam content detected | Audit content; check SpamAssassin score |
| 421 4.7.0 IP throttled | Sending too fast | Slow down, distribute IPs |
| 550 5.7.605 (Microsoft) | DBL hit | Check Spamhaus listing |
Where bounces actually go: Return-Path and SPF alignment
None of the categories above help if the bounce never reaches you. Bounces don't go to the visible From address — they go to the Return-Path, the envelope sender (MAIL FROM) from the SMTP transaction. Your ESP usually sets it, often rewriting it to its own bounce domain, and the receiving server records the final value as the Return-Path: header. If that address points at a mailbox nobody parses, you're blind to every failure this post just taught you to classify.
| Header | Visible to recipient? | Used for |
|---|---|---|
| From: | Yes | Display, replies (unless overridden) |
| Return-Path / MAIL FROM | No (in envelope) | Bounce delivery, SPF check, DMARC SPF alignment |
| Reply-To: | Yes (when user replies) | Reply routing |
Point the Return-Path at a dedicated subdomain like em.example.com instead of your main domain. That lets you publish a tightly scoped SPF record for the sending stream, keeps bounce traffic out of corporate mailboxes, and means ESP IP rotations only ever touch the subdomain's record.
| Host | Type | Value | TTL |
|---|---|---|---|
| em.example.com | TXT | v=spf1 include:_spf.your-esp.com -all | 3600 |
The Return-Path domain is also the domain SPF authenticates — not the From domain. For SPF-based DMARC authentication, the two must align: a bounce subdomain under your organizational domain aligns in the default relaxed mode, while a Return-Path on your ESP's own domain does not align at all. In that second case you're relying on DKIM alignment alone, so a missing or broken DKIM signature takes the whole DMARC result down. The DMARC alignment guide covers strict versus relaxed rules, and the SPF cheat sheet covers the record syntax; the SPF checker will expand the include chain on your envelope domain.
Two operational details worth knowing. Most ESPs use VERP (Variable Envelope Return Path), which encodes the recipient into the bounce address — [email protected] — so every bounce maps to exactly one subscriber even when the DSN body is mangled. And bounce messages themselves are sent with an empty Return-Path (<>, the null sender) so a bounce can never trigger another bounce. Never use the null sender for your own outbound mail.
Action: failed
Status: 5.1.1
Diagnostic-Code: smtp; 550 5.1.1 The email account that you tried to reach does not exist.
Final-Recipient: rfc822; missing [at] example.comRoute this mailbox into the handler below, not a human inbox. The Status: field is where the enhanced code your parser should key on actually lives.
Designing a bounce handler
What every bounce processor should do
- 01
Parse the SMTP code AND enhanced code
Don't rely on regex over English failure text — it's inconsistent across receivers. The numeric codes are standardized.
- 02
Categorize: hard, soft, block
Hard → suppress. Soft → retry queue. Block → alert and investigate.
- 03
Suppress on hard, with timestamp and reason
Store why a recipient was suppressed. Future debugging is impossible without this.
- 04
Backoff and ceiling for soft
Standard: 4 retries over 72 hours. After ceiling, suppress with a different reason code ("prolonged soft").
- 05
Alert on spike
Sudden bounce rate change is the earliest warning of an infrastructure problem. Alert on >2x baseline within an hour.
The bounce rate ceiling
Major receivers throttle senders whose bounce rate exceeds about 5% over a rolling 24-hour window. Healthy lists run under 2%. If you're above 5%, you're either sending to a bought list or your suppression isn't working — both reputation-fatal.
Additional guidance: Email Webhooks: Handling Bounces, Complaints, and Events Programmatically
An email bounce webhook is an HTTP POST your email provider sends to your server the moment a message bounces, gets delivered, or generates a complaint — carrying structured JSON you can act on in code. The critical automation: hard bounces and spam complaints should trigger an instant write to your suppression list, so the address never gets mailed again. Done right, webhooks keep your sender reputation clean automatically; done wrong (or ignored), bounce rates climb until Gmail and Yahoo start rejecting you.
This post is the implementation guide: what the events look like, how to build the endpoint, and which automations actually matter.
The event taxonomy: what your provider will send you
Every major email API (SendGrid, Postmark, SES, Mailgun, Resend, and the rest) emits roughly the same event set. Names vary; semantics don't:
| Event | Meaning | Reputation impact | Your response |
|---|---|---|---|
queued / accepted | Provider accepted the message | None | Log for latency tracking |
delivered | Receiving server accepted it (250 OK) | Positive signal | Update message state |
deferred | Temporary failure (4xx), provider will retry | Neutral unless chronic | Monitor; investigate if persistent |
bounced (hard, 5xx) | Permanent failure — dead address, rejected | Severe | Suppress immediately |
bounced (soft, 4xx exhausted) | Temporary failures gave up | Mild–moderate | Suppress after N occurrences |
complained | Recipient hit "Report spam" | Most severe | Suppress instantly, never re-add |
unsubscribed | Opt-out via link or one-click | Neutral (it's compliance) | Sync to suppression/preferences |
opened / clicked | Tracking pixel/link fired | Weak signal | Analytics only — treat as noisy, especially since Apple Mail Privacy Protection pre-loads pixels |
Two events carry nearly all the reputation risk: hard bounces and complaints. Gmail's spam-complaint threshold sits around 0.1% (Yahoo tolerates roughly 0.3%) — sustained rates above that, and bulk mail starts getting permanently rejected (550 5.7.26 at Gmail, 550 5.7.515 at Microsoft, both enforced since 2025). Everything else in your webhook pipeline exists to serve those two suppressions quickly. The full breakdown of bounce types and which ones deserve suppression is in bounce categories and handling.
Payload anatomy: what a bounce webhook actually looks like
Payloads differ per provider, but a typical hard-bounce event:
{
"event_id": "evt_01K4XQ7F9Z2N3P8R",
"event_type": "bounced",
"bounce_class": "hard",
"timestamp": "2026-08-14T09:31:07Z",
"message": {
"id": "msg_01K4XM2T...",
"from": "[email protected]",
"to": "[email protected]",
"subject": "Your receipt for order #8412",
"tags": ["receipt", "prod"]
},
"smtp": {
"response_code": 550,
"enhanced_code": "5.1.1",
"response": "550 5.1.1 The email account that you tried to reach does not exist."
},
"metadata": {
"user_id": "77123",
"order_id": "8412"
}
}The fields that matter operationally:
- `event_id` — your idempotency key for processing (more below).
- `bounce_class` or the enhanced code (
5.1.1= mailbox doesn't exist,5.7.26= authentication rejection) — this drives the suppress-vs-retry decision.5.x.xpermanent → suppress.4.x.x→ it's a deferral in disguise. - `to` — the address to suppress. Normalize it (lowercase, trim) before writing.
- `metadata` — whatever you attached at send time. This is why you tag sends with user/order IDs: a complaint event can then trigger "email this user's account team" or "suppress this user across all streams," not just one address.
- `tags` — stream identification. A complaint on
tags: ["newsletter"]suppresses from marketing; whether it suppresses from transactional is a policy decision you make deliberately (usually: complaints suppress marketing, but transactional continues — password resets are expected mail).
For how these codes map to real-world rejection text, see SMTP error codes explained. If you're choosing the sending layer that emits these events, SMTP vs API for email sending covers why webhooks are an API-native advantage. You can inspect live event shapes in the WillItInbox API docs, which expose delivery, bounce, and complaint events across 100+ endpoints.
Endpoint requirements: fast 2xx, then process async
The single most common webhook bug: doing the work inside the HTTP handler. Your endpoint must respond with a 2xx within the provider's timeout (often 5–15 seconds) or the provider marks the delivery failed and retries — multiplying your traffic and confusing your dedupe logic.
The pattern that survives production:
- Receive — verify signature (below), return
200 OKfast. - Enqueue — push the raw payload onto a queue (SQS, Pub/Sub, Redis stream, whatever you run).
- Process — a worker consumes the queue: dedupe, classify, suppress, alert, store.
Minimal pseudocode handler:
def webhook_endpoint(request):
# 1. Verify before anything else
if not verify_signature(request.body, request.headers["X-Signature"], WEBHOOK_SECRET):
return 401
payload = parse_json(request.body)
# 2. Fast ack — no business logic here
queue.publish("email-events", payload)
return 200
def process_event(payload):
# 3. Idempotency: skip duplicates
if already_processed(payload["event_id"]):
return
mark_processed(payload["event_id"], ttl_days=30)
event = payload["event_type"]
addr = normalize(payload["message"]["to"])
# 4. The automations that protect reputation
if event == "bounced" and payload["bounce_class"] == "hard":
suppression.add(addr, reason="hard_bounce", scope="all_streams")
metrics.incr("bounce.hard")
elif event == "bounced": # soft
strikes.incr(addr)
if strikes.get(addr) >= 3:
suppression.add(addr, reason="soft_bounce_repeated", scope="all_streams")
elif event == "complained":
suppression.add(addr, reason="complaint", scope="marketing") # permanent
alert_if_complaint_rate_spike()
elif event == "unsubscribed":
preferences.opt_out(addr, topics=payload.get("topics"))
store_event(payload) # audit trailNotice what's not in the HTTP handler: databases, suppression writes, alerting. All behind the queue, where a slow database can't turn into webhook timeouts.
Signature verification: never skip this
Your webhook endpoint is a public URL that writes to your suppression list. Without verification, anyone who finds the URL can POST fake complained events and poison your list — a real attack pattern, not a hypothetical.
Every serious provider signs webhook payloads, typically HMAC-SHA256 of the raw body with a per-endpoint secret, delivered in a header:
X-Webhook-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015...Verification rules:
- Hash the raw request body, not a re-serialized parse — JSON key reordering breaks signatures.
- Compare with a constant-time function (
hmac.compare_digest, not==) to avoid timing attacks. - Enforce timestamp freshness if the provider includes a timestamp header: reject events older than ~5 minutes to kill replay attacks.
- Rotate secrets the same way you rotate API keys, especially after anyone with access leaves.
If your provider also publishes static egress IPs for webhooks, IP allowlisting is a worthwhile second layer — but it complements the signature check, never replaces it.
Idempotent processing: duplicates will happen
Providers deliver webhooks at-least-once. Timeouts, retries, and queue redeliveries on your own side all produce duplicates. Your processing must be idempotent: handling the same event_id twice must produce the same state as once.
The mechanics are simple: keep a processed-events store (Redis set with a 30-day TTL works fine — events older than that don't matter operationally), check-and-set atomically before processing. Also make the effects idempotent where possible: suppression.add() should be an upsert, not an insert that throws on duplicates and fails the whole batch.
One subtlety: dedupe on event_id, not on (recipient, event_type). A recipient can legitimately bounce from two different messages minutes apart, and you want both events in your audit trail even though only one suppression write results.
Provider-side retry semantics (know what you're tuning against)
When your endpoint fails — timeout, 5xx, connection refused — providers retry with backoff. Typical behavior across the industry: retries over 24–72 hours with exponential gaps, then the event is dropped (some providers move it to a "failed events" list you can re-pull via API).
Practical implications:
- A brief outage is recoverable. Don't page anyone for a 10-minute webhook downtime; the provider's retries will backfill.
- A long outage loses events. Beyond the retry window, you need the provider's event-polling API to reconcile. Build a nightly reconciliation job that pulls the previous day's events and diffs against what you processed. This catches both dropped webhooks and silent endpoint misconfigurations.
- Return the right status codes.
2xx= processed.5xx= retry. Never return4xxfor transient problems — most providers treat 4xx as "permanently rejected, stop retrying" and you'll silently lose events. Reserve 4xx for actual signature failures (401) and malformed payloads (400). - Mind your inbound rate limits if the endpoint sits behind an API gateway or WAF. A provider retry storm after an outage can trip rate limiters and extend the outage. The same throttling and rate-limit thinking you apply to sending applies to receiving events.
The automation that actually matters: instant suppression writes
Everything above is plumbing. Here's the water: hard bounce or complaint → suppression-list entry, in seconds, before your next send.
Why the urgency? Bounce and complaint rates are computed over rolling windows. If a list segment starts bouncing at 6% and your suppression lag is 24 hours, every campaign in that window re-hits the dead addresses, compounding the signal that got you flagged. Providers like Gmail don't grade on effort; they grade on observed behavior. A suppression write that happens in 10 seconds instead of 10 hours can be the difference between a warning-level bump and a 550 5.7.26 rejection wall.
Implementation checklist:
- Central suppression store — one source of truth, keyed by normalized address, with
reasonandtimestampfields. - Checked at send time — every send path (API, SMTP relay, CRM, support desk) must consult it. A suppression list your newsletter tool ignores is decoration.
- Synced across tools — this is where teams bleed. The ESP suppresses the address, but your marketing automation platform, your CRM sequences, and your homegrown billing mailer all keep sending. Sync suppression outward via API or scheduled export; the email suppression lists guide covers the cross-tool sync patterns in detail.
- Complaints are forever — never re-add a complainer, even if they re-subscribe. Too risky. Hard bounces can be re-added only after the address is re-verified.
The pre-send complement: suppression handles addresses that go bad; validation keeps bad addresses from entering in the first place. Running new or stale segments through bulk email validation — 12 layers including SMTP handshake, catch-all detection, and spam-trap signals — removes most hard bounces before they can become webhook events at all. Prevention plus suppression is the whole game.
Alerting thresholds: catch the spike before the providers do
Your webhook stream is also your early-warning system. Wire alerts on rolling rates, not absolutes:
| Signal | Warning | Page someone |
|---|---|---|
| Hard bounce rate | >1% over 1h | >2% over 1h |
| Complaint rate | >0.05% over 24h | >0.1% (Gmail's threshold) |
| Deferral rate to one provider | >10% over 1h | >30% — you're being throttled |
| 550 5.7.26 / 5.7.515 rejections | Any sustained occurrence | Immediately — auth is broken |
The action on a page-worthy spike is uncomfortable but correct: pause the affected stream, diagnose, then resume. Sending through a bounce spike to "finish the campaign" trades a short delay for weeks of reputation repair. Because your events carry tags, you can pause surgically — kill the onboarding drip, keep receipts flowing.
Store every event, even the boring delivered ones, for at least 90 days. When deliverability degrades gradually (and it usually degrades gradually), the historical event data is how you find the week it started and what changed.
Before any of this touches production traffic, exercise the whole event flow in the email sandbox: trigger sends, bounces, and complaints against a safe environment and watch your endpoint verify, dedupe, and suppress — so the first complaint your pipeline ever handles isn't a real one at 2 a.m.
Frequently asked questions
Last updated August 2, 2026.
Sources reviewed
- Email sender guidelines(official)
- Sender requirements and recommendations(official)
Factual review: June 13, 2026 by WillItInbox Editorial.
Keep reading