Skip to content

Email Delivery Issues — UI Diagnostic Checklist

When messages don't arrive — late, bounced, missing entirely — the AcelleMail dashboard surfaces nine signals that tell you whether the issue is sending-side or recipient-side. Walk the checklist, fix from the UI in most cases.

The 60-second dashboard check

Before assuming the worst, open AcelleMail and walk these four screens. 80% of delivery cases resolve from the UI without needing the operator.

1. Look at the campaign's tracking log

Open the campaign → Tracking log tab. Every send attempt is here with status + timestamp:

Tracking log — per-message audit

What the rows tell you:

  • All recent timestamps, all "Delivered" → AcelleMail sent successfully. The issue is recipient-side (spam folder, blocked domain). Skip to Sending server test below.
  • Recent timestamps, mix of "Bounced" + "Deferred" → receiving servers are slow or rejecting. Open the bounce log next.
  • Tracking log is empty / old → the campaign never actually sent. See Campaign stuck in Sending.

2. Read the bounce log

Same campaign → Bounces tab. The DSN code column tells you why each message failed:

Bounce log — mixed hard/soft

  • Most bounces are "5.1.1 User unknown" → stale list, addresses no longer exist. Normal at <5%. Run Subscribers → Email verification before next send.
  • Most bounces are "5.7.x" (delivery not authorized) → your sending IP or domain is being blocked. Move to step 3.
  • Most bounces are "4.x.x" (temporary) → AcelleMail retries automatically. Wait — no action needed.

See Decoding bounce messages for the full DSN code reference.

3. Check sending server health

Settings → Sending servers → click the active server. The detail page shows the three authentication signals:

Sending server config — auth chips

What to verify:

  • SPF, DKIM, DMARC chips all green — receiving servers can confirm you're authorized to send.
  • Daily quota not exhausted — if you sent your daily limit, the rest queues to tomorrow.
  • Bounce + complaint rates < 5% — sustained higher rates throttle your IP.

If any chip is red, click Verify domain → AcelleMail walks you through the DNS records to add at your DNS host.

4. Run the live diagnostic test

Same sending server detail → Test button. Sends a single message to an address you specify:

Sending server test diagnostic

Send to your own personal email. Three outcomes:

  • Arrives in Inbox → sending setup is OK. The original issue is content-side (spam-trigger words) or recipient-side (their filter). See Why emails go to spam.
  • Arrives in Spam → authentication or reputation issue. Open the message → Show original (Gmail) → check Authentication-Results header.
  • Doesn't arrive at all → IP outright blocked. Check the sending IP at mxtoolbox.com → blocklist lookup.

Common UI fix paths

Signal in dashboard Likely cause What to do
Tracking log empty hours after launch Queue worker idle Operator: restart workers (see Advanced)
Most bounces "5.1.1" (User unknown) Stale list Run email verification, prune addresses
Most bounces "5.7.x" (Delivery not authorized) IP/domain blocked Check sending server Verify domain; if SPF/DKIM/DMARC all green, IP blocklist issue
SPF or DKIM chip red on sending server DNS records missing Click Verify domain → follow wizard → update DNS at registrar
Test diagnostic doesn't arrive IP on blocklist mxtoolbox.com lookup → delisting request per blocklist's process
Sending server quota hit Daily limit exceeded Wait for reset OR provision additional sending server in pool
All to ONE domain failing (e.g. @example.com only) That receiver is blocking you specifically Investigate FBL complaints from that domain; pause sends to it until reputation recovers

When to escalate to the operator

The dashboard exposes everything most users need. Escalate to the server operator when:

  • Tracking log is empty AND the campaign should have started >30 minutes ago (queue worker dead)
  • Test diagnostic doesn't arrive AND the IP is verified clean on blocklists (deeper SMTP / DNS / firewall issue)
  • Daily quota is large enough but campaigns still stop mid-send (PHP-FPM worker exhaustion, database lock)
Advanced: server-side checks for the operator

When dashboard checks point at the worker or server-side delivery, the operator's checklist:

Worker health:

ps aux | grep "queue:work" | grep -v grep
# Expect at least one process for each --queue= pool the supervisor config defines.

php artisan queue:size
# Expected: dropping toward zero as the worker processes the backlog.

php artisan queue:failed | wc -l
# >0 means jobs gave up after their tries. Investigate with:
php artisan queue:failed

Sending server connection test from the CLI:

php artisan tinker --execute='
  \$server = \App\Model\SendingServer::where("status","active")->first();
  echo "Driver: " . \$server->type . "\n";
  echo "Host: " . \$server->host . "\n";
  echo "Test result: " . json_encode(\$server->test()) . "\n";
'

SMTP smoke (raw connectivity from the AcelleMail host to the SMTP gateway):

# Replace with your sending server's host + port.
nc -zv email-smtp.us-east-1.amazonaws.com 587
# Expected: Connection succeeded. Failure = firewall blocking outbound SMTP.

TLS handshake verification:

openssl s_client -connect smtp.sendgrid.net:587 -starttls smtp -crlf < /dev/null \
  2>/dev/null | grep -E "(subject|issuer|verify)"
# Expected: valid cert chain. Self-signed or expired = TLS rejection.

DNS record check (run from the AcelleMail host to verify the records that receivers actually see):

dig TXT yourdomain.com +short | grep spf
dig TXT default._domainkey.yourdomain.com +short
dig TXT _dmarc.yourdomain.com +short

If any of those return empty, the published records aren't propagated. Wait 24h after publishing; if still empty, the DNS host hasn't accepted the record.

Log triage:

tail -200 /home/acelle/domains/acellemail.com/storage/logs/laravel.log | grep -iE "(send|smtp|fail|reject)"
tail -200 /var/log/mail.log    # if MTA = local Postfix

Sending server queue lag — for high-volume installs, the per-server queue depth tells you which sending server is the bottleneck:

php artisan tinker --execute='
  foreach (\App\Model\SendingServer::where("status","active")->get() as \$s) {
    echo \$s->name . ": " . \$s->getCurrentDailySendingQuotaUsage() . " / " . \$s->daily_quota . " today\n";
  }
'

A server at 100% quota is the cause of queue spillover into the next day.

Related articles

16 comments

9 comments

  1. Brian
    Is there a way to detect cause #6 (lost DB connection) before workers wedge? Looking for a heartbeat metric to alert on
    1. Admin
      There's no built-in heartbeat metric for that today, so you'd have to build the signal yourself. The cheapest version: a scheduled job every minute that does a trivial `SELECT 1` on the same connection the workers use and writes a timestamp somewhere outside the DB (Redis key, or a file), then alert when that timestamp goes stale by more than 2-3 minutes. That catches the connection dying before the wedge shows up as a sending backlog, since a wedged worker stays "running" from supervisor's point of view and won't trip a process alert. The other thing worth watching is the gap between queued messages and delivered ones. If queue depth is climbing while delivery count is flat, that's usually cause #6 in progress even if nothing has errored yet. Exposing a proper worker heartbeat is on the list, it just isn't there now. If you build the `SELECT 1` probe and it works well for you, send it over and I'll fold the pattern into the article.
  2. Thuỳ
    thanks for grounding this in actual source — much better than the generic laravel advice you find on stack overflow.
    1. Admin
      That was the point of writing it. Generic "check your SMTP creds" advice doesn't help when the actual failure is a sending server marked inactive or a bounce rule quietly quarantining the subscriber, and you can only tell those apart by looking at where the code decides. One caveat: the file paths in here drift when we refactor, so trust the described behaviour over the exact line numbers. If you hit a delivery failure mode the checklist doesn't cover, tell me what you saw and I'll add the branch.
  3. Lucas
    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.
  4. Ahmed
    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
      Per-campaign log file only, which is why laravel.log looks silent. Campaign runs write to their own channel under storage/logs/campaign/, one file per campaign, so anything about resuming or force resuming lands there and never touches the main log. If you want it in laravel.log too you'd have to add the default channel to that logger's stack, but it gets noisy fast on a big send. Easier to just tail the campaign file while it runs.
  5. David
    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
      Not configurable today, they're compiled into the runtime rather than read from config. If a flow exceeds them you won't get an error, the view just stops advancing past the bound, which is exactly the kind of thing that looks like a delivery problem but isn't. Worth ruling out before you go down the checklist in this article. If your customer is actually hitting it, send me the flow size and I'll take a look at making the bounds settable. It's a small change, I just haven't had a real case to justify it.
  6. Priya
    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. Daniel
    This article saved me about 4 hours of debugging today. The diagnostic order at the top is exactly the workflow I needed...
    1. Admin
      The order isn't arbitrary. It's sorted by how cheap the check is and how often it's the actual cause, so you rule out the boring stuff (server status, credits, list state) before you go digging into headers and bounce logs. Most delivery tickets we get die in the first three steps. If you hit a failure mode the checklist walked you past, tell me which one and where it should slot in. That's the part of the article I'd most like to get wrong less often.
  8. Yuki
    Bookmarking this. Wish I had it last month when our queue backed up on a Sunday night.
    1. Admin
      Weekend queue backups are the worst version of this because nobody's watching and the retry curve just keeps stacking. The checklist here is written for the after-the-fact case, where you're staring at a stuck queue and working backwards. What it doesn't cover yet is catching it early, so if you can tell us what your Sunday night looked like (queue depth, whether the sending server was throttling or actually erroring), we'll use it to add a section on the warning signs before the backup gets bad.
  9. Olufemi
    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. If supervisord never comes back after reboot, there's no worker, so nothing writes to the log you'd think to check, and the campaign just sits at "sending" with the counter frozen. Nothing looks broken, it just stops. `systemctl enable supervisor` after install is easy to miss because supervisor works fine until the first reboot, which might be months later. Same trap with cron if you set it up by hand. I'll add a step to Cause #2 for it: check `systemctl is-enabled supervisor` returns `enabled`, not just `systemctl status` showing running, and check that the campaign's last activity timestamp is moving rather than trusting the status label.

More in Troubleshooting