Laravel queue worker supervisor setup that survives production
Supervisor is a process monitor that keeps your php artisan queue:work commands running and restarts them if they crash or exit. The minimum you need is one [program:laravel-worker] block that runs queue:work with --sleep, --tries, and a --max-time, set to autostart and autorestart, plus supervisorctl reread and update to load it. Add queue:restart to your deploy script so workers pick up new code, and align --timeout with your queue retry_after value.
Skip the manual setup. DeployManage provisions your server and ships zero-downtime deploys on any cloud.
$ get started freeA Laravel queue worker is just a long running PHP process started by php artisan queue:work. It boots the framework once, then loops forever pulling jobs off the queue. The problem in production is obvious the first time it happens: the process dies, nobody notices, and jobs quietly pile up for hours. Supervisor solves that. It watches your worker processes, restarts them the moment they exit, and starts them again after a server reboot. This guide walks through a real config, the flags that actually matter for timeouts and memory, restarting workers on deploy, running several queues with priorities, and where Horizon fits.
What does Supervisor do for Laravel queue workers?
Supervisor is a process control system for Linux that keeps defined programs alive. For Laravel it runs your queue:work command, monitors it, and relaunches it automatically if it crashes or exits, including after a reboot. Running queue:work directly in an SSH session works until you close the terminal, and a background & or nohup gives you no supervision at all. Supervisor is the piece that turns a fragile foreground command into a service you can trust. If you would rather not maintain the config file by hand, DeployManage can provision and monitor Supervisor-backed workers for you, but it helps to understand the moving parts first.
The minimum Supervisor config for a Laravel queue worker
Create a program file at /etc/supervisor/conf.d/laravel-worker.conf. This is the standard shape, adjusted for a real app path and user:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --queue=default --sleep=3 --tries=3 --max-time=3600
directory=/var/www/app
autostart=true
autorestart=true
stopasignal=SIGTERM
stopwaitsecs=3600
user=deploy
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
stdout_logfile_maxbytes=50MB
A few lines carry most of the weight. command is the exact worker invocation, using an absolute path to artisan so Supervisor does not depend on your shell. user should match the account that owns your app files, so the worker writes logs and storage with the right permissions. numprocs tells Supervisor how many identical copies to run (four here), and process_name with %(process_num)02d gives each copy a unique name like laravel-worker_00. redirect_stderr folds errors into the same log so you have one place to look. The big one people miss is stopwaitsecs: set it at least as high as your longest job so Supervisor waits for a job to finish instead of killing it mid-run.
Once the file is in place, load it:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
reread reloads config files from disk, update applies the changes and starts any new programs, and start laravel-worker:* brings up every process in the group. Check status any time with sudo supervisorctl status, and tail the log with tail -f /var/www/app/storage/logs/worker.log.
How do I keep a Laravel queue worker running?
Run it under Supervisor with autostart=true and autorestart=true. Those two settings tell Supervisor to launch the worker when Supervisor itself starts (so it comes back after a reboot) and to relaunch it immediately if the process exits for any reason. That is the whole point of a process monitor: you never rely on a human noticing that a worker died. Do not run queue:work by hand in a terminal for production, since it stops the moment the session closes.
How do I set queue worker timeout and memory limits?
Use three flags together, and keep them consistent with your queue config. --timeout=60 caps how long a single job may run before the worker kills it. --tries=3 sets how many attempts a job gets before it is marked failed. --max-time=3600 and --max-jobs=1000 tell the worker to exit gracefully after roughly an hour or after a number of jobs, which is how you fight PHP memory growth: a fresh process reclaims leaked memory, and Supervisor restarts it instantly. There is also --memory=128, which stops the worker when it exceeds that many megabytes.
The subtle trap is the relationship between --timeout and the retry_after value in config/queue.php. Always keep retry_after longer than --timeout. If retry_after is shorter, the queue can release a job for a second worker to pick up while the first worker is still processing it, and you get the same job running twice. A common safe pairing is --timeout=60 with retry_after set to 90.
How do I restart queue workers after deploying?
Run php artisan queue:restart at the end of your deploy. Because a worker boots the framework once and then loops, it holds your old code in memory and will keep running the previous release until it is restarted. The queue:restart command sends a signal that tells each worker to finish its current job and then exit gracefully; Supervisor sees the exit and starts a fresh worker with the new code. Add it to your deploy script right after composer install and your migrations:
php artisan migrate --force
php artisan config:cache
php artisan queue:restart
This is exactly the kind of step that is easy to forget when you deploy by hand, which is one reason teams reach for a panel that manages queue workers for you and folds the restart into every release automatically.
How do I run multiple queues with priorities and numprocs?
List queues in priority order in a single --queue flag. The worker drains queues left to right, so --queue=high,default,low empties everything on high before touching default, and clears default before low. That is how you make sure a password reset email is not stuck behind ten thousand report jobs.
When one queue needs its own dedicated capacity, define a second program block instead of stuffing everything into one. For example, a fast notifications queue and a slow media queue can each get their own worker pool with different numprocs and --timeout values:
[program:laravel-worker-notifications]
command=php /var/www/app/artisan queue:work redis --queue=notifications --sleep=3 --tries=5 --timeout=30
numprocs=6
; ... plus the standard autostart, user, log lines
[program:laravel-worker-media]
command=php /var/www/app/artisan queue:work redis --queue=media --sleep=3 --tries=2 --timeout=600
numprocs=2
Set numprocs based on how much concurrency the work needs and what the server can handle. Each process is a full PHP interpreter using real memory and CPU, so more is not automatically better. Start conservative, watch queue depth and server load, and add processes only where jobs are actually backing up. Background jobs are also where a lot of AI features now live: teams queue up model calls and agent runs so a slow response never blocks a web request. If those jobs act on untrusted input, it is worth keeping those agents safe from prompt injection before the work ever reaches the queue.
Why does my Laravel queue worker stop processing jobs?
The usual causes, in rough order of frequency: the worker is running old code because you deployed without queue:restart; jobs are timing out and being marked failed while retry_after is misaligned; a fatal error killed the process and Supervisor is not configured with autorestart=true; the queue connection in .env does not match the queue you are pushing jobs to (for example jobs go to redis but the worker listens on database); or the worker exhausted memory and exited without a monitor to bring it back. Check sudo supervisorctl status first, then read storage/logs/worker.log and the failed_jobs table. Nine times out of ten it is a missing restart or a connection mismatch.
What is the difference between queue:work and queue:listen?
queue:work is a long lived process that boots the framework once and keeps it in memory, which makes it fast and the right choice for production. Because the app stays in memory, code changes are not picked up until you restart, hence queue:restart on deploy. queue:listen reboots the framework for every job, so it always runs the latest code without a restart, but it is much slower. Use queue:listen only in local development where the convenience beats the overhead. In production, always use queue:work under Supervisor.
Do I need Supervisor if I use Horizon?
Yes, but only one Supervisor program instead of many. Horizon is Laravel's dashboard and process manager for Redis queues. It handles worker balancing, autoscaling, per-queue configuration, metrics, and failed job retries, all defined in config/horizon.php rather than in Supervisor files. What Horizon does not do is keep its own master process alive, so you still run a single Supervisor block for php artisan horizon, and Horizon spawns and manages the actual workers underneath it:
[program:horizon]
command=php /var/www/app/artisan horizon
autostart=true
autorestart=true
stopwaitsecs=3600
user=deploy
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/horizon.log
Note the missing numprocs: you run exactly one Horizon master, and it manages worker concurrency for you. On deploy, call php artisan horizon:terminate instead of queue:restart so Horizon shuts down gracefully and Supervisor relaunches it with fresh code. Horizon requires Redis. If you are on a database or SQS queue, stick with plain queue:work and the multi-program Supervisor setup above.
Putting it together
The reliable production pattern is small: run queue:work under Supervisor with autorestart on, cap runtime with --max-time or --max-jobs to recycle memory, keep --timeout under retry_after, order queues by priority, and always call queue:restart on deploy. Get those five right and workers stop being the thing that breaks at 3am. If you would rather not hand-edit Supervisor configs across a fleet, a managed server management panel can provision the worker processes, monitor them, and wire the deploy-time restart into every release, so the config above becomes something you review rather than something you babysit.
Frequently asked questions
How do I keep a Laravel queue worker running?
Run it under Supervisor with autostart=true and autorestart=true. Supervisor launches the worker when the server boots and relaunches it instantly if the process crashes or exits, so a dead worker never goes unnoticed. Never run queue:work by hand in a terminal for production, because it stops the moment the SSH session closes.
What is the difference between queue:work and queue:listen?
queue:work boots the framework once and stays in memory, making it fast and correct for production, but it needs a restart to pick up new code. queue:listen reboots the framework for every job, so it always runs the latest code without a restart, but it is much slower. Use queue:listen only in local development.
How do I restart queue workers after deploying?
Run php artisan queue:restart at the end of your deploy script. Workers hold your old code in memory, so without a restart they keep running the previous release. The command signals each worker to finish its current job and exit gracefully, and Supervisor then starts a fresh worker loaded with the new code.
Why does my Laravel queue worker stop processing jobs?
Common causes are a deploy without queue:restart leaving stale code, timeouts misaligned with retry_after, a fatal error with no autorestart, a queue connection in .env that does not match where jobs are pushed, or memory exhaustion with no monitor. Check supervisorctl status, worker.log, and the failed_jobs table first.
Do I need Supervisor if I use Horizon?
Yes, but only one program block. Horizon manages Redis workers, balancing, and autoscaling itself, yet it does not keep its own master process alive. Run a single Supervisor program for php artisan horizon with autorestart on, and use horizon:terminate on deploy so it restarts gracefully with fresh code.