Laravel Queue Worker Keeps Stopping on Your VPS? Here's the Permanent Fix

Laravel Queue Worker Keeps Stopping on Your VPS? Here's the Permanent Fix

Here's the sequence almost everyone hits at least once: you SSH into your VPS, run php artisan queue:work to test that jobs process correctly, watch a few jobs go through, and close the terminal. An hour later, emails aren't sending, notifications aren't firing, and the queue table is quietly filling up with jobs that will never run — because the worker died the moment your SSH session ended.

This isn't a bug. queue:work is designed to run in the foreground of whatever process started it. Nothing about it is "permanent" until you tell something else to keep it alive. Here's the correct way to do that on a plain VPS, why the quick fixes people reach for first don't actually solve it, and how to confirm it's really working before you walk away.

Why nohup and "just run it in the background" don't fully work

The most common first attempt looks like this:

nohup php artisan queue:work --daemon > /dev/null 2>&1 &

This does survive an SSH disconnect — nohup is built for exactly that. But it quietly fails in three other ways that matter in production:

  • It doesn't restart on crash. If the worker throws a fatal error, hits a memory limit, or the VPS reboots, it's gone. Nothing brings it back until you notice and SSH in manually.
  • It doesn't survive queue:restart. Laravel's own deployment-safe restart command signals workers to finish their current job and exit — that's intentional. But with nohup, nothing is watching to start a replacement, so "restart" quietly becomes "stop."
  • It's easy to end up with duplicate workers. Run the nohup command twice by habit — once today, once after a deploy — and now two workers are pulling from the same queue, which shows up later as jobs occasionally running twice.

The --daemon flag itself is also worth flagging: it was removed after Laravel 5.2. On any current Laravel version, queue:work already runs in long-lived mode by default — including that flag does nothing except signal that the guide you copied it from is out of date.

The actual fix: Supervisor

Supervisor is a process manager built for exactly this job: start a process, keep it running, restart it the instant it dies, and let you control it without babysitting an SSH session.

1. Install it

sudo apt update
sudo apt install supervisor

2. Create a config file for the worker

sudo nano /etc/supervisor/conf.d/laravel-worker.conf

Paste in a config matching your setup:

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

A few of these lines matter more than they look:

  • numprocs=2 runs two worker processes in parallel. Start at 1–2 on a small VPS and raise it only if your queue actually backs up — each process is a full PHP worker sitting in memory.
  • user=www-data should match whichever user owns your application files, so the worker can write logs and read your .env without permission errors.
  • stopwaitsecs=3600 gives a job up to an hour to finish cleanly before Supervisor force-kills it during a restart — raise this if you have jobs that legitimately run long, or you'll cut them off mid-execution.
  • --max-time=3600 tells the worker to exit gracefully after an hour so Supervisor can start a fresh process — a cheap guard against slow memory creep in long-running PHP processes, and the current replacement for the old --daemon flag's job.

3. Start it

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

reread picks up the new config file, update applies it, and start actually launches the processes — skipping any one of the three is the most common reason people run this and see nothing happen.

Confirm it actually survives a disconnect

Don't just trust that it's running — prove it, in this order:

sudo supervisorctl status

You should see laravel-worker_00 and laravel-worker_01 (for numprocs=2) listed as RUNNING, with an uptime counter. Then close your SSH session entirely, wait a minute, reconnect, and check the status again — the uptime should have kept counting instead of resetting. If it reset, something restarted the process, which means autorestart is working but something else (a crash loop) is happening; check the log file next.

Finally, dispatch a real job — from a queued email, a queued notification, or php artisan tinker with a job you already have — and confirm it actually processes:

tail -f /var/www/your-app/storage/logs/worker.log

Restarting workers safely after a deploy

Because workers boot your application once and keep it in memory, they won't notice new code until they're restarted — deploying a fix and wondering why it's not live is almost always this. Add this to the end of your deploy script:

php artisan queue:restart

This doesn't kill anything abruptly — it signals each worker to finish its current job, exit cleanly, and Supervisor immediately starts a replacement running the new code. This is exactly why Supervisor and not nohup: nohup has nothing listening for that signal, so with nohup this command would just stop your queue.

When Supervisor isn't the right layer

If you're already running your app in Docker containers — including the multi-app setup in our guide to hosting multiple apps on one VPS with Docker and Caddy — don't add Supervisor inside the container on top of Docker's own process management. Instead, run the queue worker as its own container or service with restart: unless-stopped in your docker-compose.yml; Docker is already doing Supervisor's job at the container level, and stacking both adds complexity without adding reliability.

Commands and configuration in this guide were verified against Laravel's current queue documentation and Supervisor's official docs in August 2026.

Comments 0

Be the first to comment.

Leave a comment