Laravel queues move slow work — emails, notifications, image processing, API calls, exports — out of the web request into background workers. In production those workers must run continuously, restart if they crash, and reload when you deploy new code. Supervisor is the classic tool for that; systemd works too.

1. Choose a queue connection

In .env:

QUEUE_CONNECTION=redis
  • redis — fast and reliable; recommended. Install Redis and php-redis (or use the predis package). See installing Redis.
  • database — no extra service; run php artisan make:queue-table && php artisan migrate (older versions: queue:table). Fine for moderate volume.
  • sync — runs jobs immediately in the request; development only.

2. Test a worker manually

cd /var/www/laravel
php artisan queue:work --tries=3 --timeout=90

Dispatch a job from your app and watch it process. Stop with Ctrl+C.

3. Supervisor configuration

sudo apt install -y supervisor

/etc/supervisor/conf.d/laravel-worker.conf:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/laravel/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --timeout=90
directory=/var/www/laravel
user=deploy
numprocs=2
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
stopwaitsecs=3600
redirect_stderr=true
stdout_logfile=/var/www/laravel/storage/logs/worker.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=5
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status

Key options:

  • numprocs — number of worker processes; increase for higher throughput (each uses memory).
  • --max-time=3600 — the worker exits after an hour and Supervisor starts a fresh one, which limits memory growth.
  • --timeout — kills a job running longer than this. Must be less than the queue’s retry_after (in config/queue.php), or jobs may run twice.
  • stopwaitsecs — how long Supervisor waits for a job to finish when stopping; set it above your longest job.
  • user — run as the same user that owns the app files (not root).

More on Supervisor itself: Supervisor process control.

Or with systemd

A template unit runs several instances:

# /etc/systemd/system/[email protected]
[Unit]
Description=Laravel queue worker %i
After=network.target redis-server.service

[Service]
User=deploy
WorkingDirectory=/var/www/laravel
ExecStart=/usr/bin/php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --timeout=90
Restart=always
TimeoutStopSec=3600

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now laravel-worker@1 laravel-worker@2

4. Restart workers on every deploy

Workers keep your application code in memory — after deploying, they keep running the old code until restarted. Add to your deploy script:

php artisan queue:restart

Workers finish their current job and exit; Supervisor/systemd starts them again with the new code. (It uses the cache to signal workers, so make sure your cache driver is shared, e.g. Redis or file.)

5. Failed jobs

Jobs that exceed --tries go to the failed_jobs table:

php artisan queue:failed
php artisan queue:retry all
php artisan queue:retry 5
php artisan queue:forget 5
php artisan queue:flush

Implement a failed() method on jobs to alert or clean up. Monitor the failed jobs count.

6. Multiple queues and priorities

php artisan queue:work redis --queue=high,default,low

Jobs on high are processed first. Or run separate worker programs per queue, so slow low jobs never block urgent ones.

7. Horizon (Redis queues)

Laravel Horizon provides a dashboard and code-driven worker configuration for Redis queues — balancing, metrics, failed job management. With Horizon, Supervisor runs a single php artisan horizon process (and you use php artisan horizon:terminate on deploy instead of queue:restart). Protect the dashboard (/horizon) with authentication in production.

Troubleshooting

  • Jobs not processed — workers not running (supervisorctl status), wrong QUEUE_CONNECTION, or workers listening on a different queue name.
  • Changes in code not picked up — forgot queue:restart.
  • Jobs run twice — --timeout ≥ retry_after.
  • Memory grows — use --max-jobs/--max-time; look for leaks in long-running jobs.
  • Permission errors in logs — worker runs as a different user than the web server; align users and storage/ permissions.

Related: deploying Laravel with Nginx and Laravel scheduler.

  • Laravel
  • PHP
  • Queues