Running Laravel Queues in Production with Supervisor on Ubuntu

BI
Billing Support
· May 23, 2026 · 7 min read

A practical, senior-engineer guide to keeping Laravel queue workers alive in production with Supervisor on an Ubuntu VPS, plus deploy hygiene and the scheduler.

Laravel queues let you push slow work — sending email, generating PDFs, calling third-party APIs — out of the request cycle so your app stays fast. But a queue is only as reliable as the worker draining it. In development you might run php artisan queue:work in a terminal and forget about it. In production that approach falls apart the moment the worker crashes, the server reboots, or you close the SSH session.

The fix is a process manager. On Ubuntu the standard choice is Supervisor: it starts your workers on boot, restarts them if they die, runs several copies for concurrency, and captures their logs. This guide walks through a production-ready setup on an Ubuntu 22.04/24.04 VPS, the kind you can spin up on Momo Cloud.

Why a queue worker needs a process manager

A queue worker is a long-running PHP process. It boots the framework once, then loops forever pulling jobs off the queue. That design is fast, but it has consequences:

  • It must run continuously. If the process exits — fatal error, out-of-memory, a deploy — jobs stop being processed until something restarts it.
  • It must survive reboots. A VPS gets rebooted for kernel updates or after a power control action in your panel. The worker has to come back automatically.
  • It must scale. One worker processes one job at a time. Throughput comes from running several workers in parallel.

Supervisor handles all three. You describe the worker once in a config file and Supervisor keeps it alive.

Prerequisites

This assumes a deployed Laravel app on an Ubuntu VPS, served by Nginx or Apache as www-data, with a queue backend (Redis is the common choice). Open your VPS from the server list in the client area at cloud.momo.tz, then connect over SSH or use the in-browser Console. The app in the examples lives at /var/www/app — adjust the paths to match yours.

First, set your queue connection in .env so jobs actually go onto Redis rather than running synchronously:

QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

Tip: If QUEUE_CONNECTION is left as sync, jobs execute inline during the request and the worker has nothing to do. Set it to redis (or database) and run php artisan config:clear after editing .env.

Install Supervisor

sudo apt update
sudo apt install supervisor -y

Enable it so it starts on boot, and confirm it is running:

sudo systemctl enable --now supervisor
sudo systemctl status supervisor

Create the worker program config

Supervisor reads program definitions from /etc/supervisor/conf.d/. Create one for your queue worker:

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

Paste in the following, adjusting paths, queue connection, and numprocs to suit your app and CPU:

[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=2
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
stopwaitsecs=3600

What each directive does:

  • command — the long-running worker. --sleep=3 pauses three seconds when the queue is empty; --tries=3 retries a failing job up to three times before it lands in failed_jobs; --max-time=3600 gracefully restarts the worker after an hour to release any leaked memory.
  • process_name + numprocs — runs two named worker processes (laravel-worker_00, laravel-worker_01) for concurrency. The %(process_num)02d placeholder keeps each name unique.
  • autostart / autorestart — start on boot and restart automatically if the process exits.
  • user=www-data — run as the same user that owns the app, so log files and storage stay writable.
  • redirect_stderr + stdout_logfile — merge errors into one log you can tail.
  • stopwaitsecs=3600 — give a worker up to an hour to finish its current job before Supervisor force-kills it. This must be at least as large as your longest job; otherwise a graceful stop becomes a hard kill mid-job.

Warning: Make sure storage/logs is writable by www-data or Supervisor will fail to open the log file. sudo chown -R www-data:www-data /var/www/app/storage fixes the common permission error.

Load the config and start the workers

Tell Supervisor to re-read its configuration, apply the changes, then start the worker group:

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

The :* suffix targets every process in the group. Check that they are running:

sudo supervisorctl status

You should see both processes in the RUNNING state. Watch jobs being processed by tailing the log:

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

Deploy hygiene: always restart workers

Here is the subtle part that trips people up. Because a worker boots the framework once and runs forever, it keeps the old code in memory after you deploy. New jobs will run against the previous release until the process restarts. The fix is one command at the end of every deploy:

php artisan queue:restart

This does not kill workers abruptly. It signals each one to exit gracefully after its current job finishes; Supervisor then restarts it — now loading your new code. Add it to your deploy script after composer install and migrations:

cd /var/www/app
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan queue:restart

Tip: Forget queue:restart and you will spend an afternoon debugging "why is my fix not working" while workers happily run last week's code. Make it the last line of every deploy.

queue:work versus queue:listen

Both process jobs, but they differ in how they handle code:

Aspectqueue:workqueue:listen
Framework bootOnce, stays in memoryRe-boots for every job
PerformanceFast (low overhead)Slow (high overhead per job)
Picks up new codeOnly after restartAutomatically
Use in productionYes — pair with queue:restartNo — avoid

Use queue:work under Supervisor in production for its speed, and rely on queue:restart after deploys. queue:listen exists mainly for convenience while developing; its per-job reboot makes it far too slow for real workloads.

The scheduler: a companion cron entry

If your app uses Laravel's task scheduler (the schedule() method or scheduled commands), it needs a single cron entry that runs every minute. The scheduler itself decides which tasks are due — you only register the one heartbeat. Edit the crontab for the app user:

sudo crontab -u www-data -e

Add this line:

* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

That is the entire scheduler setup. Cron fires schedule:run once a minute; Laravel checks your defined tasks and runs whatever is due. Keep this separate from Supervisor — the scheduler is short-lived and cron-driven, while queue workers are long-running and Supervisor-driven.

Organising multiple queues by priority

As an app grows you will want urgent jobs (password resets, payment callbacks) to jump ahead of bulk work (newsletters, report exports). Define a separate Supervisor program per priority so a flood of low-priority jobs never starves the important ones:

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

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

Tip: One worker group per queue priority keeps capacity predictable. Give the high queue more processes than default so latency-sensitive work always has a free worker.

After editing, run reread and update again to apply the new groups.

Wrapping up

With Supervisor in place your Laravel queue workers start on boot, restart on crash, run several at once for throughput, and pick up new code the moment you run php artisan queue:restart. Add the one-line cron entry for the scheduler and you have a complete, production-grade background-processing setup on a single Ubuntu VPS.

Need a server to run it on? Spin up an Ubuntu 22.04/24.04 VPS from the client area at cloud.momo.tz — you get root access, an in-browser console, and power controls — and if you get stuck, our team is available 24/7 in English and Swahili through a support ticket.

BI
Written by

Billing Support

Sharing insights on hosting, cloud, security and the technology that powers your business online.