Decoding Bounce Messages — Read the Tracking Log Like a Pro

Hard bounces, soft bounces, deferred — what AcelleMail's bounce log is actually telling you, and which bounces you can ignore vs. which need action. Read the UI first, dive into the SMTP codes only when needed.

Read the bounce log from the campaign report

AcelleMail tracks every send attempt — including bounces — in the campaign's Tracking log. Open any sent campaign → Bounces tab:

Campaign bounce log — hard/soft mix with DSN codes

Each row shows:

  • Recipient email — who didn't receive
  • Bounce typeHard (permanent — invalid address, domain doesn't exist) or Soft (temporary — mailbox full, server unreachable)
  • DSN reason — the receiving server's response, in plain-ish English
  • Time — when AcelleMail tried to deliver

What AcelleMail does automatically

You don't need to manually clean up most bounces. AcelleMail's bounce handling runs automatically:

  • Hard bounces are marked as unsubscribed after the first occurrence. They're removed from future sends to the same list. No action needed.
  • Soft bounces are retried with backoff (next campaign batch). After 5 consecutive soft bounces, AcelleMail auto-promotes to hard bounce (treats as permanent).
  • Spam complaints (recipient hit "This is spam") flow through SMTP-vendor webhooks (SES, SendGrid, Mailgun, Postmark) and immediately unsubscribe the subscriber.

The campaign overview shows the totals:

Campaign overview — Failed count in stats

If Failed is <5% of the list, you're fine — that's the industry-typical churn from invalid + abandoned addresses on any list. If Failed is >10%, something needs attention (see "When to take action" below).

When to take action — UI-driven decisions

What you see in the log What it means What to do
Most bounces say "User unknown" / "Mailbox does not exist" Stale list — addresses that no longer exist Normal at <5%. Above that, run Subscribers → Email verification before next send.
Most bounces say "Mailbox full" / "Quota exceeded" Temporary issue on the recipient side Wait — AcelleMail retries automatically. No action.
Bounces concentrate on ONE recipient domain (e.g. all @example.com fail) The receiving server may be blocking your sending IP Check Settings → Sending servers for the IP being used. If multiple domains block you, IP reputation issue. See Why are my emails going to spam folder.
Bounces say "Authentication failed" / "DMARC failure" Your domain authentication (SPF / DKIM / DMARC) isn't configured correctly Run Settings → Domains → Verify and follow the wizard.
Bounces growing during a send (started low, spiking) Your sending IP is being rate-limited mid-send Sending server warm-up is incomplete OR daily quota exceeded. Wait or split campaign.

The Subscribers view — clean up manually if needed

If you want to manually mark certain addresses unsubscribed (e.g. an entire former-customer list), open Subscribers in a list, filter by status, and bulk-change:

  • Filter by status Failed to see hard-bounced addresses.
  • Bulk-select rows → top toolbar → Change statusUnsubscribed.
  • The list count + future sends update immediately; no separate suppression step needed.

Common UI signals — quick recap

  • Failed count <5% of list → ignore, AcelleMail handles it
  • Failed count >10% of list → run email verification before next send
  • Bounces clustered on one domain → IP/DNS issue, check sending server
  • "Authentication failed" bounces → fix SPF/DKIM/DMARC
Advanced: SMTP DSN code reference for operators

SMTP responses follow RFC 3463 (Enhanced Mail System Status Codes). Three digits, separated by dots: X.Y.Z.

Class (X) — severity:

  • 2.X.X — Success (delivered)
  • 4.X.X — Persistent transient failure (will retry — soft bounce)
  • 5.X.X — Permanent failure (will NOT retry — hard bounce)

Subject (Y) — what failed:

  • X.1.X — Addressing status (recipient address)
  • X.2.X — Mailbox status (full, disabled, doesn't exist)
  • X.3.X — Mail system status (overloaded, down)
  • X.4.X — Network and routing status
  • X.5.X — Mail delivery protocol status
  • X.6.X — Message content or media status
  • X.7.X — Security or policy status (authentication, blacklist, DMARC)

Common codes you'll see:

Code Meaning Action
2.0.0 Message accepted for delivery Success — no bounce
4.2.2 Mailbox full Soft — retried automatically
4.7.0 General temporary failure (often greylisting) Soft — retried
5.1.1 User unknown (recipient address doesn't exist) Hard — auto-unsubscribe
5.1.2 Domain doesn't exist Hard — auto-unsubscribe
5.2.0 Mailbox issue (suspended, archived) Hard
5.2.1 Mailbox disabled Hard
5.7.1 Delivery not authorized (blocked) — common when sender on blocklist Hard — investigate IP reputation
5.7.26 DMARC failure (alignment policy) Hard — fix DKIM/SPF setup

Reading the full DSN in the log:

The "DSN reason" column shows the full response string from the receiving server, e.g.:

550 5.1.1 <bob@example.com>: Recipient address rejected: User unknown in local recipient table
  • 550 — SMTP-level rejection code (matches 5.X.X class)
  • 5.1.1 — Enhanced status code (RFC 3463)
  • The text after — server's human-readable reason

Querying the bounce log from the database for offline analysis:

php artisan tinker --execute='
  \App\Model\BounceLog::where("campaign_id", <id>)
    ->where("dsn_status", "like", "5.7%")
    ->select("recipient", "dsn_status", "diagnostic_code")
    ->get()
    ->each(fn($r) => print("{$r->recipient}\t{$r->dsn_status}\t{$r->diagnostic_code}\n"));
'

Filtering FBL (Feedback Loop) entries — recipient marked your message as spam:

php artisan tinker --execute='
  \App\Model\FeedbackLog::orderByDesc("created_at")
    ->limit(50)->get()
    ->each(fn($r) => print("{$r->recipient}\t{$r->feedback_type}\t{$r->created_at}\n"));
'

When the FBL volume rises sharply, your IP reputation is in jeopardy — pause campaigns to that segment, investigate content + frequency.

Related articles

23 Kommentare

14 Kommentare

  1. joel.anders.se
    this article saved me about 4 hours of debugging today. the diagnostic order at the top is exactly the workflow i needed.
    1. admin (bearbeitet)
      Appreciate it. If anything in this needs updating, ping us — we revisit articles every few months
  2. linhpm.devs
    What about the case where campaign:rerun itself crashes silently? We had cron running but :rerun was failing on a deleted customer and just bailing out. No alerts
  3. emma.whitaker
    If you're on Ubuntu 22.04 with the default cron package, the time zone is UTC even if /etc/timezone says otherwise. Bit us once — the scheduled job timing was 7 hours off.
    1. admin
      Good tip. The Cloudflare-outbound-rat-limit case is something we hadn't documented.
  4. jmorrison.itop…
    one more thing worth adding: if you use cloudflare in front, make sure to whitelist the worker ips for outbound — we had retries failing because cloudflare was rate-limiting our own outbound traffic to ses.
  5. phuong.mai.hn
    bookmarking this. Wish I had t last month when our queue backed up on a Sunday night.
    1. admin
      Glad it landed. Drop suggestions in the comments and we'll incorporate them on the next refresh.
  6. linhvu.dev
    is there a way to detect cause #6 (lost DB connection) before workers wedge? Looking for a heartbeat metric to alert on.
    1. admin (bearbeitet)
      depends on your version. 5.x supports it natively; 4.x needs a config flag set in `.env`. we'll note this caveat in the article on the next pass.
  7. aisha.khan.pak
    adding to this: we had a campaign stuck for 6 hours one time. turned out the running_pid was alive but the worker was deadlocked on a slow mysql query. ps showed it as running, kill -9 was the only fix. now we monitor for stale running_pid > 30 min.
    1. admin
      Great real-world detail. Your point about stale running_pid > 30 min as an alert is something we should add to the diagnostic flow.
  8. lucas.bernard.…
    Confirming the campaign:rerun auto-fix actually works. We had a worker OOM mid-batch last week, walked away thinking we'd need to manually intervene, came back in 15 minutes and it had recovered itself.
  9. m.schmidt78
    Question: in step 4, the campaign log line about 'force resuming' — does that show up in laravel.log or only the per-campaign log file? Our laravel.log seems silent on this.
    1. admin
      For your specific case, I'd recommend testing with `--dry-run` first. The behavior under high load isn't 100% deterministic and we want you to see your own pattern before committing.
  10. lequan.saigon
    We hit cause #5 last uarter — SES sandbox limits we didn't know about. The 'wait it out' advice is right. We tried aggressive retries first and it just made things worse
  11. ravi.kumar.del…
    Pro-tip: set `pm.max_children` on your PHP-FPM pool to at least 2x your supervisor worker count. Otherwise even ack-fast endpoints choke when the queue rate goes up.
  12. d.cohen.tlv
    Curious if the 200-row / 32-advance bounds are configurable. We have one customer with very large automation flows and I wonder if they hit this.
    1. admin
      Honest answer: it depends on your provider. SES handles it gracefully; Mailgun is stricter. We'll add a provider-by-provider table in the next revision.
  13. sobrien.kw
    We hit cause #5 last quarter — SES sandbox limits we didn't know about. The 'wait it out' advice is right. We tried aggressive retries first and it just made things worse
  14. y.yamamoto
    when you say to bump wait_timeout to 86400, does that need a mysql restart or is it dynamic? we're on rds so restarts are expensive.
    1. admin
      theres no built-in way today. two workarounds: (1) cron + custom script polling the api every n minutes, (2) webhook-driven if your event source supports it. most operators go with #2
    2. admin (bearbeitet)
      We don't recommend that approach in production. It works in dev but has subtle race conditions under concurrent load. Stick with the documented pattern.

More in Troubleshooting