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.

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

6 comments

5 comments

  1. v.petrova.ru
    Saving this one. We're about to hit the volume tie where we need to think about queue tuning.
  2. danrey.dev
    Tip for high-volume installs: monitor your failed_jobs table size, not just count. We had a queue migration that left 50k stale failed rows that started slowing reads. Truncate periodically.
  3. tnovak.cz
    Saving this one. We're about to hit the volume tier where we need to think about queue tuning.
    1. admin (edited)
      Thanks. Pass it along if it helps your team
  4. cw.dev.sh
    Moved from database queue to Redis last month at ~800k emails/day. Worker throughput went up ~40%. MySQL CPU dropped from 60% to 18% baseline. Highly recommend the migration once you're past 500k...
  5. aditi.s.bom
    We do automated backups to S3 nightly. wp-cli-style. Restore tested quarterly. The article's emphasis on testing restores cannot be overstated.

More in Server Management