Skip to content
Production Supervisor configs for Laravel queue workers, schedule:work, and Horizon — with the gotchas around stopwaitse...

Supervisor for Laravel: Queue Workers, Scheduler, Horizon

Al Amin Ahamed

Al Amin Ahamed

Senior Software Engineer

· Updated 7 hours ago 8 min read

Supervisor is a process manager. For a Laravel app, it keeps queue workers, the scheduler, and any custom long-running processes alive. systemd can do the same job, but Supervisor's config language is friendlier and its log handling is built-in.

Here's the configuration I run in production.

Why Not Just php artisan queue:work?

If you ssh to your server and run php artisan queue:work, the moment your shell disconnects the worker dies. nohup and & postpone the problem; they don't solve it. When the worker crashes (and it will — OOM, network blip, deploy), nothing restarts it.

Supervisor solves both: it daemonizes processes and auto-restarts them on failure.

Directory Layout

/etc/supervisor/conf.d/
  laravel-worker.conf      # queue workers
  laravel-scheduler.conf   # scheduler
  laravel-horizon.conf     # if using Horizon instead of queue:work
  laravel-reverb.conf      # if using Reverb websockets

One file per concern. Easier to reason about than one mega-file.

Queue Worker

INI
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/portfolio/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --max-jobs=1000
autostart=true
autorestart=true
user=deploy
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/portfolio/shared/storage/logs/worker.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
stopwaitsecs=3600

Key choices:

  • numprocs=2 — two worker processes. Bump up if you have CPU headroom and your queue depth grows.
  • --max-time=3600 — workers exit cleanly after an hour. Supervisor restarts them. This prevents memory leaks from accumulating indefinitely.
  • --max-jobs=1000 — same idea, but counted by jobs processed.
  • stopwaitsecs=3600 — when Supervisor sends SIGTERM, give the worker a full hour to finish its current job before sending SIGKILL. Critical for long jobs.
  • %(process_num)02d — Supervisor expands this to 00, 01 per process. Required when numprocs > 1.

The Scheduler

INI
[program:laravel-scheduler]
command=php /var/www/portfolio/current/artisan schedule:work
autostart=true
autorestart=true
user=deploy
redirect_stderr=true
stdout_logfile=/var/www/portfolio/shared/storage/logs/scheduler.log
stdout_logfile_maxbytes=50MB

schedule:work is the modern replacement for the cron entry that runs schedule:run every minute. Supervisor keeps it alive; the command's internal loop ticks every 60 seconds and dispatches scheduled tasks.

If you prefer cron (some teams do for visibility):

CRON
* * * * * deploy cd /var/www/portfolio/current && php artisan schedule:run >> /dev/null 2>&1

I prefer Supervisor — one process manager, all logs in one place.

Horizon

If you're using Horizon, replace laravel-worker with:

INI
[program:laravel-horizon]
command=php /var/www/portfolio/current/artisan horizon
autostart=true
autorestart=true
user=deploy
stopwaitsecs=3600
redirect_stderr=true
stdout_logfile=/var/www/portfolio/shared/storage/logs/horizon.log

Horizon spawns its own worker children based on config/horizon.php. Supervisor manages the parent. numprocs=1 (the default) is correct here — Horizon does the multiplexing.

Deploy-Time Restart

Workers cache code. After a deploy, they're running the old code until restarted:

BASH
php artisan queue:restart
sudo supervisorctl restart laravel-worker:*

queue:restart writes a sentinel to the cache. Workers check the sentinel between jobs and exit gracefully. Supervisor restarts them with the new code.

Critical: restart (not stop then start). Restart is one operation; stop-then-start has a window where workers are completely down.

Permissions

Supervisor runs as root by default. The user=deploy directive drops privileges before exec'ing. The deploy user needs:

  • Read access to the codebase (/var/www/portfolio/)
  • Write access to logs (storage/logs/)
  • Permission to write to Redis and the database
BASH
sudo useradd -r -s /bin/bash deploy
sudo chown -R deploy:deploy /var/www/portfolio

Status & Logs

BASH
sudo supervisorctl status
# laravel-worker:laravel-worker_00   RUNNING   pid 12345, uptime 2:14:08
# laravel-worker:laravel-worker_01   RUNNING   pid 12346, uptime 2:14:08
# laravel-scheduler                  RUNNING   pid 12347, uptime 2:14:08

sudo supervisorctl tail -f laravel-worker:laravel-worker_00

The tail -f is invaluable when debugging a job that misbehaves only in production.

Healthchecks

I run a side script that asserts Supervisor processes are RUNNING and pages me if anything is FATAL:

BASH
#!/usr/bin/env bash
EXPECTED=("laravel-worker:laravel-worker_00" "laravel-worker:laravel-worker_01" "laravel-scheduler")

for proc in "${EXPECTED[@]}"; do
    status=$(sudo supervisorctl status "$proc" | awk '{print $2}')
    if [ "$status" != "RUNNING" ]; then
        curl -X POST "$ALERT_WEBHOOK" -d "Supervisor process $proc is $status"
    fi
done

Cron'd every 5 minutes. Stupid simple, works.

Share 𝕏 in
Al Amin Ahamed

Al Amin Ahamed

Senior software engineer & AI practitioner. 5+ years shipping Laravel platforms, WordPress plugins, WooCommerce extensions, and AI-driven products.

About me →

More from the blog

Need this kind of work shipped?

Available for freelance and consulting.

Laravel platforms, WordPress plugins, WooCommerce extensions, and AI integrations.