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. i.rossi.mil
    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
      Thanks for the numbers. Worth pulling into a follow-up post on volume-tier sizing. tbh
  2. y.yamamoto
    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
      for your specific case, id 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.
  3. tnovak.cz
    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.hassan.c…
    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
      Good question — and one that comes up often enough we should add an FAQ section. Short answer: yes for the common case; the exception is when you're running custom plugins that override the default behavior.
  5. aisha.khan.pak
    Thanks for grounding this in actual source — much better than the generic Laravel advice you find on Stack Overflow
  6. akira.tnk88
    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
  7. hung.nguyen.it
    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.
  8. danrey.dev
    Bookmarking this. Wish I had it last month when our queue backed up on a Sunday night
  9. lucas.bernard.…
    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
      Good question. The campaign:rerun audit writes to laravel.log only when the audit decides to force-resume — pure noop runs are silent. We'll add an info-level heartbeat in a future Acelle release to make it easier to monitor
  10. priya.iyer.ops
    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
      Great real-world deail. Your point about stale running_pid > 30 min as an alert is something we should add to the diagnostic flow

More in Troubleshooting