Let's Connect

Racks of production servers in a data center, representing always-on Laravel queue worker infrastructure

If you run Laravel queue workers in production by SSHing in and typing php artisan queue:work, you do not have a queue system — you have a time bomb. The moment you log out, the SSH session ends and the worker dies. tmux is no better: a deploy, an OOM kill, or a reboot takes it down and your jobs stop processing silently. No error, no alert, just emails that never send and webhooks that never fire. The fix is to put Laravel queue workers under Supervisor: it keeps them alive, restarts them when they crash, and brings them back automatically after the box reboots.

Why does php artisan queue:work die when I close my terminal?

A queue worker is a long-lived foreground process. When you start it inside an SSH session, it is a child of that session. Log out and the shell sends SIGHUP to its children — the worker exits. Running it in tmux survives logout, but tmux is still just a process on the box: it does not survive a reboot, a server crash, or your deploy script killing stray PHP processes. And a single bare worker has no concurrency and no restart policy, so one fatal error and the whole queue stalls until a human notices.

What you actually want is something whose entire job is to run your worker, watch it, and restart it under defined conditions. On Ubuntu that tool is Supervisor. It runs as a systemd service, so once you enable it on boot, your workers come back with the machine — no human in the loop.

Terminal showing a running process being monitored, illustrating a supervised long-lived queue worker
A supervised worker is a process whose lifecycle something else owns — not your SSH session.

How do I install Supervisor and make it start on boot?

Install it from apt, then enable the systemd unit. That second command is the part everyone forgets — without it, Supervisor itself does not come back after a reboot, and neither do your workers.

bash
sudo apt update
sudo apt install -y supervisor

# This is the line that survives reboots — enable + start the service
sudo systemctl enable supervisor
sudo systemctl start supervisor

# Sanity check
sudo systemctl status supervisor

What goes in the Supervisor program config for a Laravel worker?

Each managed program gets its own .conf file under /etc/supervisor/conf.d/. Here is a config that runs four workers against a Redis queue. If you have not set up the Redis backend yet, see my notes on Redis caching and queue strategies for getting the connection right first.

/etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
stopwaitsecs=3600

A few of these flags carry real weight in production:

  • numprocs=4 runs four identical worker processes for concurrency. process_name with %(process_num)02d gives each one a unique name so Supervisor can track them individually.
  • user=www-data runs workers as the same user as PHP-FPM, so storage and log files do not end up root-owned and unwritable.
  • stopasgroup=true and killasgroup=true make sure that when Supervisor stops a worker, the signal reaches the whole process group — not just the parent PHP process, leaving orphaned children behind.
  • autorestart=true relaunches a worker if it exits for any reason, including a fatal error or hitting --max-time.
  • redirect_stderr=true folds stderr into the same log file so you have one place to look when a job blows up.

How do I apply the config and start the workers?

Supervisor does not auto-detect new config files. You tell it to re-read the directory, apply the diff, then start the new program group. The :* suffix targets every process in the group.

bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*

# Confirm all four are RUNNING
sudo supervisorctl status

A healthy status output shows laravel-worker:laravel-worker_00 through _03 all in the RUNNING state with an uptime. If one sits in FATAL, tail storage/logs/worker.log — it is almost always a permissions issue on the log file or a bad path to artisan.

Why does stopwaitsecs matter, and how does it relate to --timeout?

When Supervisor stops a worker, it sends SIGTERM and waits stopwaitsecs seconds for the process to exit cleanly. If the worker is still running after that window, Supervisor sends SIGKILL — which terminates it mid-job, with no chance to mark the job failed or release it back to the queue. That is how you get jobs stuck in a half-finished state.

The rule: stopwaitsecs must be at least as long as your longest-running job, and it should line up with the queue:work --timeout (which defaults to 60 seconds). If a job can legitimately run for 50 minutes, set --timeout above that and set stopwaitsecs to at least 3600 so Supervisor's graceful-stop window outlasts the job. Mismatch these and a deploy will SIGKILL a worker that was three seconds from finishing a payment-processing job.

A worker you never restart on deploy is a worker running last week's code — Supervisor keeps it alive, but it is alive and wrong.Md Raihan Hasan

Why do my workers run stale code after a deploy?

This is the gotcha that bites everyone once. Queue workers are long-lived: a worker boots the framework into memory and then loops, processing jobs without ever reloading your code. Deploy a fix and the running workers keep executing the old, in-memory version until they restart. Your web requests get the new code instantly; your queue silently runs the old code for hours.

The fix is one command in your deploy pipeline. queue:restart sets a flag the workers check between jobs — each one finishes its current job, then exits gracefully, and Supervisor immediately relaunches it with the fresh code.

bash
# Run this at the END of every deploy, after the new code is in place
php artisan queue:restart

This belongs as the last step of your deployment script, right after migrations and cache rebuilds. If you automate releases, drop it into your GitHub Actions CI/CD pipeline so no one has to remember it. While you are tuning workers, keep --max-time=3600 (and optionally --max-jobs=1000) in the command: long-lived PHP processes accumulate memory, and bounding their lifetime lets Supervisor recycle them before they balloon. For a larger setup, run a dedicated worker tier separate from your web servers — I cover that split in my AWS production architecture write-up.

Code on a screen representing a deployment pipeline where queue:restart is wired into the release step
queue:restart belongs in the deploy script — not in your memory.

What about Horizon, and where does the scheduler fit?

If you are on Redis, Laravel Horizon is the higher-level option. Instead of hand-writing numprocs and worker flags, you configure queues and process balancing in config/horizon.php, and Horizon auto-scales workers across queues and gives you a real dashboard with throughput, runtime, and failed-job metrics. You still run Horizon itself under Supervisor — the program just runs php artisan horizon instead of queue:work, and on deploy you call horizon:terminate instead of queue:restart.

One thing people conflate: the scheduler is not a queue worker. The Laravel scheduler runs from a single cron entry that fires once a minute and dispatches due tasks. It is separate from your Supervisor-managed workers — workers process queued jobs continuously, while the scheduler kicks off scheduled commands on a clock.

crontab -e (as the deploy user)
* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

Get this stack right once and your queue stops being a liability. Supervisor enabled on boot means workers survive reboots; stopwaitsecs aligned with your job timeout means graceful stops never SIGKILL work in flight; and queue:restart in your deploy means workers always run current code. The failure mode for queues is silence — no 500, no stack trace, just work that quietly never happens. Build the supervision layer so that silence never gets a chance to start.