What this is for#
AcelleMail uses Laravel queues to dispatch campaign sends, automation triggers, list imports, and bounce processing asynchronously. The default queue driver is the database (jobs table in MySQL) — fine for the smallest hobby installs, but it bottlenecks fast:
- Polling cost: each worker polls the
jobs table every --sleep=3 seconds with a SELECT FOR UPDATE that briefly locks the table
- Contention under load: 15 workers all polling + 1 web request inserting jobs into the same table is a hot lock
- No native priority: the database driver iterates jobs in
id order; switching queues = a separate scan
Redis as the queue driver fixes all three: O(1) push/pop via Redis LIST primitives, no MySQL contention, native per-queue ordering. At any volume above ~5k sends/day, Redis is the right choice.
This guide walks the install, the Acelle config, the tuning knobs that matter, and the monitoring hooks.
Prerequisite: working AcelleMail install per the Ubuntu 24.04 canonical guide — Redis is Step 4 of that guide.
Step 1 — Install Redis 7#
# Ubuntu / Debian
sudo apt update && sudo apt install -y redis-server
sudo systemctl enable --now redis-server
# Rocky / RHEL
sudo dnf module reset redis
sudo dnf module install -y redis:remi-7.2
sudo systemctl enable --now redis
Verify the version + that it responds:
redis-server --version | head -1 # Expect 7.x
redis-cli ping # Expect PONG
Don't use anything older than Redis 7 in 2026. Redis 6 is end-of-life; Redis 5 has long been EOL.
Step 2 — Tell systemd to manage Redis properly (Ubuntu/Debian)#
sudo sed -i 's/^supervised .*/supervised systemd/' /etc/redis/redis.conf
sudo systemctl restart redis-server
This lets systemd correctly track Redis health for restart-on-failure semantics. Skip on Rocky — Remi's package is already systemd-supervised.
Step 3 — Configure AcelleMail to use Redis#
Edit /var/www/acellemail/.env:
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=null
REDIS_DB=0
REDIS_CACHE_DB=1
CACHE_DRIVER=redis
SESSION_DRIVER=redis
Use 127.0.0.1, not localhost. PHP's Redis client treats localhost as a Unix-socket hint on some systems; using the IP forces TCP, which is what Redis is configured to accept.
Clear the cached config and restart:
cd /var/www/acellemail
sudo -u www-data php artisan config:clear
sudo supervisorctl restart acellemail-master:* acellemail-worker:*
After the workers restart, queue operations route through Redis. The web UI should show no difference — Redis is a drop-in replacement.
Step 4 — Understand AcelleMail's queue layout#
AcelleMail's routes/console.php + the shipped supervisor templates show the actual queue names in use:
| Queue |
Used for |
Throughput |
default |
Misc Laravel jobs + low-priority Acelle jobs |
Low (~1-10/s) |
import |
List imports — long-running, single-job-per-import |
Very low (1-2 concurrent) |
high |
Campaign send jobs from "Priority" campaigns |
High (10-100/s) |
batch |
Bulk send jobs from normal campaigns |
Very high (50-500/s) |
single |
Transactional + single-recipient sends |
Medium (5-50/s) |
automation |
Automation-flow steps for individual subscribers |
Medium |
automation-dispatch |
Bulk automation trigger evaluation |
Medium |
The supervisor setup splits these into two pools: master handles import,default; worker handles high,batch,single,automation,automation-dispatch. Don't change the split unless you understand the underlying contention model.
Step 5 — Redis tuning for the AcelleMail workload#
Edit /etc/redis/redis.conf (Ubuntu) or /etc/redis/redis.conf + /etc/redis.conf on Rocky:
Memory cap#
# Set to ~25% of system RAM as a hard ceiling
maxmemory 2gb
# CRITICAL for queues: never evict queue keys
maxmemory-policy noeviction
noeviction is non-negotiable for queues. Any other policy (allkeys-lru, volatile-ttl, etc.) means Redis can silently delete pending jobs when memory pressure rises — campaigns will appear sent but recipients receive nothing. With noeviction, Redis returns an error to the producer (the web request) instead, which Acelle handles correctly.
If you also use Redis for cache, use a separate logical DB (REDIS_CACHE_DB=1) and configure per-DB eviction via SELECT 1; CONFIG SET maxmemory-policy allkeys-lru — but the global default must stay noeviction.
Persistence#
Redis has two persistence modes — pick based on your tolerance for data loss:
# RDB snapshots (default) — saves a binary dump every N seconds with M+ changes
save 900 1
save 300 10
save 60 10000
# AOF (Append-Only File) — writes every change to disk
appendonly no
For an AcelleMail queue:
- RDB only (default) is acceptable. Worst case: lose up to 60 seconds of queue data on power loss = at most a minute of un-sent emails for affected jobs. Acelle's retry logic handles this gracefully (jobs that were enqueued but lost just don't run; the campaign-level state ensures they get re-enqueued on
campaign:rerun).
- AOF on halves throughput but loses at most 1 second on power loss. Only worth it if you cannot tolerate any job loss (rare for email sending).
Don't enable AOF "just in case" — it doubles disk I/O and is rarely needed for queues.
TCP + I/O#
tcp-keepalive 300
io-threads 4 # If you have ≥ 4 cores
io-threads 4 parallelizes socket reads (Redis 6+ feature). On a 4-vCPU server it gives ~30% throughput gain at high concurrency. Don't set above your core count.
Reload Redis after each change:
sudo systemctl restart redis-server
Step 6 — Monitoring#
The five Redis metrics worth watching:
# 1. Queue depths — should be 0-100 steady-state
for q in default import high batch single automation automation-dispatch; do
printf "%-22s %s\n" "$q" "$(redis-cli llen queues:$q)"
done
# 2. Memory usage vs maxmemory
redis-cli info memory | grep -E "used_memory_human|maxmemory_human"
# 3. Commands per second (throughput)
redis-cli info stats | grep total_commands_processed
# Run twice with a 5-second gap, divide the difference by 5
# 4. Slow log — any command > 10ms is suspect
redis-cli slowlog get 10
# 5. Persistence health
redis-cli info persistence | grep -E "rdb_changes_since|aof_enabled"
For production, push these to your monitoring stack:
redis_exporter (Prometheus) — runs as a separate process, scrapes Redis INFO every 15s
- Grafana dashboard
redis-exporter quickstart — drop-in
- DataDog Redis integration — same metrics, fewer moving parts if you're already on DataDog
Alert thresholds:
| Metric |
Warn |
Critical |
| Memory usage |
> 70% of maxmemory |
> 90% |
| Queue depth (any single queue) |
> 5,000 sustained 5 min |
> 50,000 |
| Slow commands per minute |
> 5 |
> 50 |
| Connected clients |
> 50 (usually ~20 on AcelleMail) |
> 200 (indicates a leak) |
Step 7 — When to add Laravel Horizon (optional)#
Laravel Horizon is a web dashboard for Redis-backed queues. It's not bundled with AcelleMail, but installs cleanly as a Composer dependency:
cd /var/www/acellemail
sudo -u www-data composer require laravel/horizon
sudo -u www-data php artisan horizon:install
What you get:
- Real-time dashboard at
/horizon showing per-queue throughput, failed jobs, recent jobs
- Auto-balancing (
balance: auto in config/horizon.php) — dynamically reallocates workers across queues based on load
- Tags + filters — search "all failed jobs for customer X in the last hour"
Trade-off: Horizon's horizon daemon replaces your supervisor queue:work lines. You run a single supervisor entry for php artisan horizon and Horizon manages the worker pool internally. If you go this route, delete the Acelle two-tier supervisor configs and put a single Horizon supervisor config in their place. Don't run both — they'll fight over jobs.
Recommended if you operate 3+ AcelleMail installs or you need finer per-queue observability. For a single install, the bare supervisor setup is simpler and works.
Step 8 — When to add a separate Redis box#
At Large tier (5M+ sends/month) you start to want Redis on its own host:
- Application server and Redis on separate boxes → web requests don't compete with workers for the Redis socket
- A dedicated Redis box can use 100% of its RAM for cache+queue (vs sharing with PHP-FPM + MySQL)
- Easier to add HA via Redis Sentinel (1 primary + 2 replicas + Sentinel for automatic failover)
- Hosting providers' managed Redis (DigitalOcean Managed Redis, AWS ElastiCache, Hetzner Cloud Redis) is the lowest-ops route
Update the AcelleMail .env:
REDIS_HOST=10.0.0.5 # private network IP of your Redis box
REDIS_PORT=6379
REDIS_PASSWORD=... # set this for any non-loopback Redis!
If you expose Redis on a public network or even a shared private network, set requirepass in redis.conf and use REDIS_PASSWORD in .env. A passwordless Redis on a public IP is one of the most-scanned exploits on the internet.
See Scaling for 100K+ Emails Per Day for the full multi-box architecture.
Common issues#
| What you see |
Likely cause |
Fix |
| Workers report "Connection refused" to Redis |
Redis not running or wrong host/port in .env |
sudo systemctl status redis-server; verify .env values; php artisan config:clear |
| Queue depth keeps growing despite running workers |
Worker pool too small for load |
Increase supervisor numprocs; consider Horizon balance: auto |
| "Jobs being run twice" (recipients get duplicate emails) |
Worker --tries was changed from 1 to ≥ 2 |
Restore --tries=1 in supervisor config; jobs that fail under tries=2 risk duplicate sends on retry |
| Out of memory error from Redis |
maxmemory too low, or queue backlog grew |
Increase maxmemory; verify maxmemory-policy=noeviction (so jobs aren't silently lost); diagnose backlog source |
redis-cli works but app can't connect |
Redis bound to 127.0.0.1 but app uses different network namespace |
Verify bind 127.0.0.1 matches what app uses; for Docker, change to bind 0.0.0.0 + use internal network |
| AOF growing huge |
AOF rewrite cron not running |
redis-cli BGREWRITEAOF; or disable AOF if not needed |
| All queues empty but campaigns stuck |
Cron stopped firing (campaign:schedule isn't dispatching) |
Check cron is running per supervisor + cron guide |
FAQ#
Can I use Memcached instead of Redis? Technically — for cache yes; for queues no. Laravel's queue system supports Redis + database + SQS + Beanstalkd, not Memcached. Stick with Redis.
What about KeyDB / Dragonfly / Garnet? All three are Redis-API-compatible alternatives that claim higher throughput. They work with AcelleMail (since Laravel just speaks the Redis protocol), but the community is smaller and edge-case compatibility isn't perfect. For a small-to-medium AcelleMail install, vanilla Redis is the safer choice; consider alternatives only if you hit a real throughput wall.
Redis on the same host as MySQL — is that bad? Fine at Small + Medium tier. At Large tier, separate them — Redis's tcp keepalives + MySQL's connection pool can compete for file descriptors at high concurrency.
How do I clear all queues without restarting? redis-cli FLUSHDB clears the current DB. Be very careful — this deletes all pending jobs (you'll lose any unsent emails currently queued). Better: use php artisan queue:flush which only clears failed jobs.
Redis 6 vs 7 vs 8? Use 7.x. Redis 8 was released June 2024 with significant changes (AGPL licence, JSON/Search/TimeSeries built in); it works but the ecosystem hasn't caught up. Most Redis-as-a-service offerings as of 2026 are still on 7.
Why does AcelleMail use --sleep=3? Without --sleep, an empty-queue worker spins at 100% CPU polling for jobs. --sleep=3 makes it wait 3 seconds between polls when the queue is empty, dropping CPU to ~0% during idle periods. With Redis + Lua-based BLPOP this is less critical than with database polling, but the 3-second sleep is harmless and consistent with Acelle's templates.
Related articles#