# Setting Up Queue Workers and Cron Jobs

> AcelleMail uses Laravel queues + a system cron to do everything beyond rendering web pages — sending campaigns, processing automations, handling bounces, refreshing stats, dispatching scheduled sends. This guide walks the production-correct two-tier supervisor split (master + worker), the cron schedule, and the operational discipline that keeps the queue healthy under load.

- Canonical URL: https://acellesend.com/kb/articles/setting-up-queue-workers-and-cron-jobs
- Author: Daniel Reyes, Technical Editor
- Category: Server Management — Cron jobs, queues, Supervisor, Redis, Nginx, performance tuning
- Content type: tutorial · Difficulty: intermediate
- Tags: Acellemail
- Published: 2026-05-03 · Last updated: 2026-08-15
- Reading time: 10 min
- Publisher: AcelleMail Knowledge Base, https://acellesend.com/kb

---

## What this is for

AcelleMail uses Laravel queues + a system cron to do everything beyond rendering web pages — **sending campaigns, processing automations, handling bounces, refreshing stats, dispatching scheduled sends, verifying senders, monitoring subscriptions**. If the queue workers aren't running or the cron isn't firing, the web UI looks fine but nothing actually happens.

This guide walks the production-correct setup as Acelle's own scaffolding defines it: a **two-tier supervisor split** with different queue priorities, a system cron line, and the operational discipline to keep it healthy.

**Prerequisites:** a working AcelleMail install per the [Ubuntu 24.04 canonical guide](https://acellesend.com/kb/articles/install-acellemail-on-ubuntu-2404) (Steps 1-7 are enough; this article details Step 8 — supervisor — and Step 9 — cron).

## Step 1 — System cron

This is the simpler one. Add a single one-minute cron line, run as the web user:

```bash
sudo crontab -u www-data -e
# Add:
* * * * * cd /var/www/acellemail && php artisan schedule:run >> /dev/null 2>&1
```

> **Rocky / RHEL:** `sudo crontab -u nginx -e` instead of `www-data`.

The Laravel scheduler runs once per minute and fans out to AcelleMail's recurring tasks. Per `routes/console.php`, these are what AcelleMail actually schedules:

| Command | Frequency | What it does |
|---|---|---|
| `campaign:schedule` | every minute | Picks up scheduled campaigns whose send time has arrived; dispatches send jobs to the queue |
| `automation:dispatch` | every 5 minutes | Evaluates active automation workflows; enqueues "trigger" jobs for subscribers who match conditions |
| `handler:run` | every 30 minutes | Bounce + FBL handler — polls the configured IMAP/SES/SNS feedback endpoints, parses bounces, marks subscribers undeliverable |
| `sender:verify` | every 5 minutes | Checks pending domain verifications (SPF/DKIM/DMARC); marks them verified once DNS propagates |
| `queue:adjust` | every minute | Auto-scaler — launches up to 20 extra worker processes if priority-queue depth crosses a threshold |
| `campaign:rerun` | every 10 minutes | Resumes campaigns that were paused due to transient failures (e.g. quota exhausted, sending server temporarily unreachable) |
| `geoip:check` | every minute | GeoIP database refresh check (low-cost; only acts when a new database is available) |
| `system:cleanup` | daily | Prunes old log entries, expired sessions, finished one-shot jobs from `jobs` table |
| `subscription:monitor` | hourly | Reconciles SaaS subscriptions with the payment provider (catches missed webhooks) |
| `update_list_stats` | daily | Recomputes list-level subscriber counts / engagement stats |

**Verify cron is wired:**

```bash
sudo -u www-data crontab -l | grep schedule:run
# Should print the cron line.

# Watch the scheduler tick in real-time (Laravel logs to storage/logs/laravel.log):
sudo tail -f /var/www/acellemail/storage/logs/laravel.log
# After 60 seconds you should see the scheduler entries.
```

If cron isn't firing, the most common cause is the wrong user — the cron must be in `www-data`'s crontab (Ubuntu/Debian) or `nginx`'s (Rocky), not your sudo user's.

## Step 2 — Supervisor (the two-tier split)

Supervisor keeps the queue worker processes alive — restart on crash, restart on deploy, automatic startup on boot.

```bash
sudo apt install -y supervisor
sudo systemctl enable --now supervisor
```

> **Rocky:** `sudo dnf install -y supervisor && sudo systemctl enable --now supervisord` — note the trailing `d` in the service name.

### Why two tiers, not one big pool

Acelle ships two distinct supervisor configs (`resources/documents/supervisor_master_config.tmpl` + `supervisor_worker_config.tmpl`) for a reason: **import jobs are slow and CPU-heavy** (parsing 200k-row CSVs, deduping against existing subscribers), while **send jobs are fast and I/O-bound** (one HTTP call to SES per email). Mixing them in a single pool means a single import job blocks 5 send jobs behind it — sending throughput collapses during list imports.

The fix: dedicate a small pool (2 workers) to `import,default` queues, and a larger pool (15 workers) to the send + automation queues.

### Master pool — for imports + default

```bash
sudo tee /etc/supervisor/conf.d/acellemail-master.conf <<'CONF'
[program:acellemail-master]
process_name=%(program_name)s_%(process_num)02d
command=/usr/bin/php /var/www/acellemail/artisan queue:work --queue=import,default --tries=1 --max-time=180
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/supervisor/acellemail-master.log
stopwaitsecs=3600
CONF
```

### Worker pool — for sends + automations

```bash
sudo tee /etc/supervisor/conf.d/acellemail-worker.conf <<'CONF'
[program:acellemail-worker]
process_name=%(program_name)s_%(process_num)02d
command=/usr/bin/php /var/www/acellemail/artisan queue:work --queue=high,batch,single,automation,automation-dispatch --tries=1 --max-time=180
autostart=true
autorestart=true
user=www-data
numprocs=15
redirect_stderr=true
stdout_logfile=/var/log/supervisor/acellemail-worker.log
stopwaitsecs=3600
CONF
```

### Apply + start

```bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start acellemail-master:* acellemail-worker:*
sudo supervisorctl status
# Expect: 2 master processes + 15 worker processes, all RUNNING
```

> **Rocky:** drop the configs at `/etc/supervisord.d/*.ini` (not `conf.d/*.conf`) and adjust the `user=` to `nginx`.

### Why `--tries=1 --max-time=180`?

These are Acelle's chosen defaults from the shipped templates, and they're right:

- `--tries=1` — a single attempt per job. Email-sending jobs aren't safely retryable (you might double-send to a subscriber); Acelle handles retries explicitly at the campaign level for the few cases where it's safe.
- `--max-time=180` — each worker exits after 180 seconds. Supervisor immediately restarts it. This bounds memory growth (PHP doesn't always release memory cleanly across requests) and ensures workers pick up any code changes from the most recent deploy within 3 minutes.

If you increase `--max-time`, you increase the worst-case memory footprint per worker. Don't go above 600s unless you've profiled memory.

## Step 3 — Sizing the pools for your workload

The 2 + 15 default from Acelle's templates is the **Medium tier** baseline (4 vCPU / 8 GB RAM). Adjust per your hardware:

| Tier | Master numprocs | Worker numprocs | Server (per [server-reqs](https://acellesend.com/kb/articles/server-requirements-and-hosting-options)) |
|---|---|---|---|
| Hobby | 1 | 2 | 1 vCPU / 2 GB |
| Small | 1 | 4 | 2 vCPU / 4 GB |
| Medium | 2 | 15 | 4 vCPU / 8 GB |
| Large | 4 | 30 | 8 vCPU / 16 GB |

**Memory math:** each worker peaks at ~256-512 MB on heavy campaigns. With `numprocs=15`, worst case is ~7.5 GB just for workers — that's why Medium tier is 8 GB RAM minimum. If your server can't fit the recommended pool, scale down (and accept slower throughput) rather than OOM-killing workers mid-campaign.

**CPU math:** each worker is mostly I/O-bound (waiting for the sending API), so they share CPU cores well. 15 workers on 4 vCPU is fine; 30 workers on 4 vCPU starts to context-switch-thrash.

**Auto-scaling:** the `queue:adjust` scheduled command launches up to 20 *extra* worker processes when the priority-queue depth crosses a threshold (defined per-campaign via the "Priority" setting in admin). This is opportunistic, not a substitute for sizing the steady-state pool correctly.

## Step 4 — Restart on deploy

Every code deploy (patch upgrade, manual git pull, Docker image rebuild) requires restarting all workers — otherwise they keep running the old code from when they started:

```bash
sudo supervisorctl restart acellemail-master:* acellemail-worker:*
```

Make this the **last step of every deploy script**. The `stopwaitsecs=3600` in the configs gives in-flight jobs up to an hour to finish gracefully before being killed (campaigns in progress aren't interrupted).

## Step 5 — Monitoring

The five things worth watching:

```bash
# 1. Workers running?
sudo supervisorctl status
# All entries should be RUNNING; any FATAL or BACKOFF needs investigation

# 2. Queue depth (Redis driver)
redis-cli llen queues:default
redis-cli llen queues:high
redis-cli llen queues:import
redis-cli llen queues:automation
# Steady-state should be 0-100. Sustained > 1000 means workers are
# under-provisioned for the load. Spiking > 10k during a campaign is
# normal; should drain within a few minutes.

# 3. Failed jobs (one-time check + ongoing)
cd /var/www/acellemail && sudo -u www-data php artisan queue:failed
# Should be empty on a healthy install

# 4. Worker logs
sudo tail -f /var/log/supervisor/acellemail-worker.log
# Watch for "Failed:" or "Error:" lines

# 5. Cron last run (via Laravel scheduler log)
sudo tail -50 /var/www/acellemail/storage/logs/laravel.log | grep -E "Scheduled|cronjob"
```

For production, wire these into your monitoring stack:

- Redis queue depths via a 1-minute Prometheus exporter or DataDog integration
- Worker count via `supervisorctl status | grep -c RUNNING` — alert if < expected
- Failed-jobs count → daily Slack notification

## Step 6 — Diagnose common failures

| Symptom | Likely cause | Diagnostic + fix |
|---|---|---|
| Campaigns stuck in "Sending" status | All workers crashed; or 0 workers running | `sudo supervisorctl status`; restart with `sudo supervisorctl restart acellemail-worker:*` |
| Imports timing out | Master pool exhausted (only 2 procs by default) | Temporarily bump master `numprocs` to 4 during the import window; revert after |
| Automations not firing | `automation:dispatch` not running (cron broken) | Verify cron user = `www-data`; check Laravel scheduler log entries |
| Failed jobs accumulating | Sending server credentials wrong, or quota exhausted | `php artisan queue:failed`; inspect each failed payload; fix the underlying cause; `php artisan queue:retry all` only after fixing |
| New code not taking effect | Workers running old code in-memory | `sudo supervisorctl restart acellemail-master:* acellemail-worker:*` |
| Worker logs flooded with "Connection refused" | MySQL or Redis down/restarting | `sudo systemctl status mysql redis-server`; check if a recent restart killed connections |
| `supervisorctl: command not found` | Supervisor not installed | `sudo apt install -y supervisor && sudo systemctl enable --now supervisor` |
| Workers eat all RAM | `--max-time` too long or memory leak | Drop `--max-time` to 120; check `storage/logs/laravel.log` for hints; profile with `htop` |

## FAQ

**Can I use systemd instead of supervisor?** Technically yes — systemd has `Restart=always` and can manage worker pools. Operationally, supervisor's `numprocs` + per-process logs + `supervisorctl restart group:*` ergonomics are noticeably better for queue-worker management. Stick with supervisor unless you have a specific reason not to.

**Why not use Laravel Horizon?** Horizon is a great web UI for Redis-backed queues — auto-balancing, real-time dashboard, failed-job UI. It's not bundled with AcelleMail; you'd install it as a Composer dependency. **It works alongside Acelle's supervisor setup** but doesn't replace the supervisor configs (Horizon needs supervisor to run *its* main process). If you're operating multiple AcelleMail installs or you want better visibility, Horizon is worth the setup overhead.

**What happens if a worker dies mid-job?** Supervisor restarts it within seconds. The job that was in-flight is re-enqueued only if it's marked as such by Laravel — with `--tries=1`, it'll instead show up in `failed_jobs`. For email-sending jobs this is correct behaviour (better one missed send than a duplicate).

**Can I run workers on a separate server?** Yes — point them at the same Redis + MySQL as the web server. This is the standard pattern at Large tier — see [Scaling for 100K+ Emails Per Day](https://acellesend.com/kb/articles/scaling-for-100k-emails-per-day).

**Why does `queue:adjust` use a `custom01` queue I've never heard of?** It's an opt-in priority queue for campaigns marked "Priority" in admin. If you never use the priority setting, `queue:adjust` is a no-op (it sees no jobs on `custom01` and exits). Keep the scheduled command — it's cheap and the feature is useful.

**What queue driver should I use?** Redis (`QUEUE_CONNECTION=redis` in `.env`). The database driver works but creates excess load on MySQL — at any reasonable send volume, Redis is significantly faster. See [Redis for Queue Processing](https://acellesend.com/kb/articles/redis-for-queue-processing).

**Can I skip supervisor and just run workers in a screen/tmux session?** For development, yes. For production, no — they'll die when the session disconnects and won't restart on reboot. Supervisor exists for exactly this.

## Related articles

- [Install AcelleMail on Ubuntu 24.04 LTS](https://acellesend.com/kb/articles/install-acellemail-on-ubuntu-2404) — sets up the prerequisites (this is Step 8 + 9 of that guide, expanded)
- [Redis for Queue Processing](https://acellesend.com/kb/articles/redis-for-queue-processing) — Redis tuning (recommended queue driver)
- [Automated Database Backups](https://acellesend.com/kb/articles/automated-database-backups) — companion ops task
- [Scaling for 100K+ Emails Per Day](https://acellesend.com/kb/articles/scaling-for-100k-emails-per-day) — when to add more workers + split off DB
- [Post-Install Hardening Checklist](https://acellesend.com/kb/articles/post-install-hardening-checklist) — secure the queue + cron infra
- [Docker Deployment Guide for AcelleMail](https://acellesend.com/kb/articles/docker-deployment-guide-for-acellemail) — Docker equivalent of supervisor (compose services)
- [Server Requirements and Hosting Options](https://acellesend.com/kb/articles/server-requirements-and-hosting-options) — pool sizing reference table

---

## Related articles

- [Redis for Queue Processing](https://acellesend.com/kb/articles/redis-for-queue-processing): AcelleMail's default queue driver is the database — fine for hobby installs, terrible for production. Switching to Redis cuts queue-dispatch latency by 10×, eliminates MySQL's `jobs`-table contention under load, and unlocks better worker auto-scaling. This guide walks the install, the Acelle config, the tuning knobs that matter, and the monitoring hooks.
- [Scaling AcelleMail for 100K+ Emails Per Day](https://acellesend.com/kb/articles/scaling-for-100k-emails-per-day): At 100K sends/day you cross the threshold where the default AcelleMail config starts to drag — queue depths climb, MySQL slow queries appear, occasional PHP-FPM 502s. This guide walks the bottleneck math, the configs to tune (queue pool size, MySQL InnoDB, PHP-FPM, nginx, Redis), when to split components onto separate hosts, and the cost/benefit at each step.
- [Automated Database Backups for AcelleMail](https://acellesend.com/kb/articles/automated-database-backups): The first backup is the cheapest you'll ever take. By the time you actually need to restore, the cost of NOT having a backup is hours of downtime, lost subscribers, and a furious customer-success conversation. This guide walks the production-correct backup setup: daily mysqldump with rotation, offsite copies, file-tree snapshots, restore drill workflow, and the monitoring that tells you when it stops working.
- [BIMI with VMC — Cost, Process, and Whether It's Worth It](https://acellesend.com/kb/articles/bimi-with-vmc-cost-walkthrough): VMC (Verified Mark Certificate) costs $1,500/yr per domain and takes 4-8 weeks to obtain. This guide walks the cost math, the trademark requirements, the alternative paths, and a decision framework for whether VMC is right for your sending program.
- [Building Your Email Marketing SaaS](https://acellesend.com/kb/articles/building-your-email-marketing-saas): The technology side of an AcelleMail SaaS is solved. The hard part is the business: who you sell to, how you price, how you reach them, and how you keep them. This guide is the GTM + pricing playbook — opinionated, market-tested patterns for launching an email-marketing SaaS on the AcelleMail stack.
