Skip to content

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 <[email protected]>: 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

15 comments

10 comments

  1. Isabella
    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
    1. Admin
      Aggressive retries against an SES throttle are the worst thing you can do, because each retry still counts as a send attempt against the quota. So you burn the ceiling on retries and starve the real sends. That's why the 454 Throttling failure section says back off rather than push. The sandbox one bites a lot of people because the bounce text doesn't say "sandbox" anywhere. You get a rejection about the recipient not being verified, or you just hit the ceiling much earlier than expected, and it reads like a deliverability problem when it's an account state problem. I'll add a line to the 454 section pointing that out, with the "check whether you're still in sandbox before you debug anything else" step first.
  2. Yuki
    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.
    1. Admin
      That's a bug, not intended. A missing customer row should skip that campaign loudly, not end the pass. Send me the customer id and rough time and I'll add the guard. Also check your cron line isn't ending in >/dev/null 2>&1.
  3. Tomas
    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.
  4. Ahmed
    when you say to bump wait_timeout to 86400, does that need a mysql restart or is it dynamic? were on rds so restarts are expensive.
    1. Admin
      Dynamic, no reboot. Change it in the RDS parameter group. Only applies to new connections though, so bounce your queue workers after.
  5. Aisha
    Thanks for grounding this in actual source — much better than the generic Laravel advice you find on Stack Overflow
  6. Hùng
    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.
  7. Akira
    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
  8. Daniel
    Bookmarking this. Wish I had it last month when our queue backed up on a Sunday night
  9. Lucas
    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
      It goes to the per-campaign log file, not laravel.log, which is why yours looks silent. Campaign-scoped lines land under storage/logs/campaign/{campaign_uid}.log, and laravel.log only picks up the run if something throws. So a force-resume that works cleanly leaves no trace in laravel.log by design. If you're tailing bounce activity, the per-campaign file is the one to watch, it interleaves the resume line with the delivery attempts around it so you can see what state the campaign was in. I'll add a note about the two log destinations to step 4, it clearly isn't obvious from the current wording.
  10. Priya
    Cause #2 (dead supervisor) hit us after a kernel-upgrade reboot. The systemctl enable bit was missing. Took 2 hours to figure out because nothing was logging
    1. Admin
      The silence is the worst part of that one. If supervisord never starts, nothing writes to the log at all, so you go looking in the bounce and tracking logs for a problem that isn't there. The only signal is negative: campaigns stuck at the same sent count, and the last tracking_log row timestamped right around the reboot. `systemctl is-enabled supervisor` after any kernel upgrade is worth a line in the article, and I'll add it to the troubleshooting section. Post-reboot checks belong there anyway, since a dead worker looks nothing like an SMTP failure but people land on this page for both.

More in Troubleshooting