Skip to content

My Campaign Is Stuck in "Sending" — What to Check

A campaign that still shows "Sending" hours after you hit send. Walk through the three UI signals that tell you what's actually happening — then the two-click fix that resolves it for most teams.

What to check from the dashboard

A campaign that says Sending hours after you launched it isn't always actually stuck — most of the time AcelleMail's auto-rerun has already picked it back up and is finishing the send in the background. The dashboard tells you whether to wait or whether to act.

Open Campaigns in the left sidebar. Look at the row for your campaign — there are three things to read:

Campaign list — Sending badge visible

  1. The status badge — "Sending" means the campaign is in progress.
  2. The "Sent" counter — does it match your list size, or is it stuck at a lower number?
  3. The "Last updated" timestamp — when did the worker last touch this campaign?

If "Last updated" is less than 10 minutes ago, AcelleMail is actively sending. Refresh in a few minutes — it'll catch up.

If "Last updated" is 30+ minutes ago and Sent is below the list size, the worker hasn't been processing this campaign — keep reading.

Open the campaign and look at the live counts

Click the campaign name. The overview page shows the per-status breakdown — Delivered, Bounced, Failed, Opened, Clicked:

Campaign overview — sending state

The two numbers that matter:

  • Sent vs. list size — if Sent < list size and not moving, AcelleMail isn't picking up the campaign for processing.
  • Failed count — if this is growing, the SMTP server is rejecting messages and your campaign is stuck on the same recipient batch.

Check the tracking log for what AcelleMail tried to send

Inside the campaign, the Tracking log tab is the per-message audit. Every send attempt — successful, bounced, deferred — has a row:

Tracking log — per-message audit

Scroll to the latest entries:

  • All rows show recent timestamps → AcelleMail is sending right now, the dashboard is just showing slightly stale counts. Refresh in 5 minutes.
  • Latest row is hours/days old → no send activity. The worker is the bottleneck (see below).
  • Latest rows show SMTP errors repeatedly → your sending server credentials, IP reputation, or DNS records need attention. Open Settings → Sending servers and click Test connection on the active server.

Common UI causes and the fix from the dashboard

What you see Likely cause What to do
Status: Sending · Sent count not moving · Tracking log empty The sending worker is idle. Ask the operator to restart workers (see Advanced below). If you're self-hosted as a solo operator, see the operator section.
Status: Sending · Sent count = part of list · Last updated >1 hour A batch failed mid-run; AcelleMail auto-rerun will pick it up next 10-minute window. Wait 15 minutes. Refresh. If Sent still doesn't move, jump to the operator section.
Status: Sending · Failed count growing fast SMTP rejection. Settings → Sending servers → Test connection. Common causes: expired API key, exceeded daily quota, IP blocked.
Status: Sending · all recipients in tracking log show "deferred" Receiving servers are throttling. Wait — deferred messages are retried automatically. If still deferred after 24h, the sending server's IP needs a warm-up. See Why are my emails going to spam.

Pause and resume from the campaign UI

If you need to stop the send (e.g. wrong list, urgent edit), the campaign overview has a Pause button while it's in Sending state. Pause → fix the issue → click Resume — AcelleMail picks up where it left off, no duplicates.

Advanced: server-side checks for the operator

If the dashboard checks above point at the worker being idle, the operator needs to verify the queue worker process is running. AcelleMail pushes campaign sends onto a Laravel queue; if the worker process dies (OOM, missing cron, supervisor not running), campaigns sit in the "sending" state indefinitely.

Quick worker check (SSH to the AcelleMail host):

# Is the queue worker process up?
ps aux | grep "queue:work" | grep -v grep

# How big is the queue backlog?
php artisan queue:size

# How many failed jobs need a retry?
php artisan queue:failed | wc -l

Restart the worker (assumes Supervisor is managing it — the recommended install pattern):

supervisorctl restart acelle-queue-worker:*

If you're running the worker manually without Supervisor:

nohup php artisan queue:work --queue=high,default,low --sleep=3 --tries=3 &

Force a campaign rerun — AcelleMail's SendCampaign console command picks up stuck campaigns in 10-minute windows. To trigger it immediately:

php artisan acelle:send-campaign --force

Cron check: AcelleMail's acelle:run command is the master scheduler. If it stopped running (cron disabled, crontab edited), campaigns never get picked up. Confirm with:

crontab -l | grep acelle:run
# Expected: * * * * * php /path/to/acelle/artisan acelle:run >/dev/null 2>&1

Inspect failed jobs:

php artisan queue:failed
# If a campaign send failed and the worker gave up after `tries` exhausted,
# retry it with:
php artisan queue:retry all

Logs to read when the dashboard counts don't match worker behavior:

  • storage/logs/laravel.log — Laravel exceptions during send
  • storage/logs/queue-worker.log (if configured) — per-job timing
  • The campaign tracking log inside the dashboard remains the source of truth for per-recipient outcome; the operator logs explain WHY the worker stalled.

Related articles

23 comments

16 comments

  1. Ahmed
    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.
  2. Tomas
    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.
  3. Lucas
    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.
  4. Joel
    sent this to my whole os team. Should be required reading before anyone touches the prod queue anyway
    1. Admin
      Tell them to check the queue worker is alive before anything else. If it's dead, every other symptom in there looks the same.
  5. Marcus
    Bookmarking this. Wish I had it last month when our queue backed up on a Sunday night.
  6. Priya
    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, not laravel.log. The force resume line is written through the campaign's own logger, so a silent laravel.log is expected and doesn't mean the resume never fired. Look in the storage logs directory for the file named after that campaign's uid, and if you can't find it, tell me your storage path and I'll check where it landed on your install. I'll add the exact location to step 4, it's clearly missing.
  7. Emma
    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.
  8. Chen
    Thanks for grounding this in actual source — much better than the generic Laravel advice you find on Stack Overflow.
  9. Linh
    i think the recommendation to wait 10-15 minutes is too patient for production. we alert at 5 minutes and our ops team triages. stuck status > 5 min usually means something real.
    1. Admin
      Fair. The 10-15 minutes in the article is written for the person with one queue worker and a list big enough that a slow batch legitimately looks stuck. If you have ops watching, 5 minutes is a better threshold, and honestly the useful signal isn't elapsed time at all, it's whether the delivered count is still moving. A campaign that hasn't incremented in 5 minutes is stuck. One that's crawling is just slow. I'll rewrite that section to lead with "is the counter moving" and mention the 5 minute alert as the production stance.
  10. Ravi
    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 constants in the automation runtime rather than settings. The old reply on this thread was pasted from somewhere else, ignore it. For what it's worth, a large flow hitting those bounds usually doesn't look like a stuck campaign. The 32-advance limit is per tick, so a subscriber just picks up where it left off on the next run. What actually leaves a campaign parked in "sending" is the queue worker dying mid-batch, which is the section above on checking worker status. If your customer is seeing flows advance far slower than expected, send me the flow size and tick interval and I'll check whether the bounds are the real bottleneck before we talk about making them tunable.
  11. Sarah
    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. lol
  12. Vera
    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.
    1. Admin
      Worth adding, but I want to get the detail right before it goes in the article. Cloudflare in front is inbound proxy, so outbound SMTP/API calls to SES shouldn't touch it unless something is routing egress through Cloudflare too. Were you on Zero Trust / WARP or a tunnel for egress? If so that's the bit I'd write up, because it's a real trap and the symptom (retries silently failing, campaign parked in "sending") looks nothing like the cause. Tell me which setup and I'll fold it into the checklist section.
  13. Mai
    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. Set it in the RDS parameter group. But existing connections keep the old value, so restart your queue workers or nothing changes.
  14. Akira
    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 dedicated DB-connection heartbeat exposed today, so what people do instead is alert on the symptom, which turns out to be good enough. A sending campaign touches its activity timestamp as pages get delivered, so if a campaign is in `sending` status and that timestamp hasn't moved in, say, 5 minutes, something upstream is wedged. Lost DB connection is one cause, a dead worker or a stalled vendor call look the same from there. If you want to separate them, run a cheap probe on the same connection the workers use (a `SELECT 1` on a cron every 30s, writing a timestamp somewhere your monitoring can read) and compare it against the campaign staleness alert. Probe stale plus campaign stale points at the DB. Probe fine plus campaign stale points at the worker or the sending server. A real per-worker heartbeat is the right fix and it's on the list, I just don't have a date for it. If you build the probe side and it works, tell me what you emitted and I'll fold it into the article, that section deserves more than "restart the worker".
  15. Brian
    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
  16. David
    This article saved me about 4 hours of debugging today. The diagnostic order at the top is exactly the workflow I needed.

More in Troubleshooting