Updated June 2026. Tested on Laravel 13 and PHP 8.4. Part of the Techalyst queue series. See also Laravel Queue Workers: How They Work.

A queue worker is a long lived process that booted your application once and keeps it in memory. That is what makes it fast, and it is also why a deploy does not reach your workers automatically. Ship new code, and your workers keep running the old code until you restart them. Getting deployment right is mostly about restarting workers at the correct moment.

Restart workers on every deploy

The command to learn is queue:restart. Put it in your deploy script after you pull new code.

php artisan queue:restart

This does not kill workers instantly. It signals each running worker to finish the job it is holding and then exit, what we call graceful termination. Your process manager (Supervisor, systemd) then starts a fresh worker, which boots the new code. A typical deploy script looks like this.

cd /home/forge/mysite.com
git pull origin main
composer install --no-interaction --prefer-dist --optimize-autoloader

php artisan migrate --force
php artisan queue:restart

After PHP-FPM reloads, visitors start using the new code while the old workers finish their current jobs and exit. Moments later the new workers are up on the new code.

Restarting Horizon

Horizon has its own command, but the idea is identical: signal the master supervisor to terminate all workers gracefully.

php artisan horizon:terminate

For this to be truly graceful, three timeouts have to line up so nothing force-kills a job mid run:

  • Your job's timeout should be shorter than the Horizon supervisor's timeout.
  • The Horizon supervisor's timeout should be longer than your longest running job.
  • If Supervisor manages the Horizon process, its stopwaitsecs should be longer than your longest job, so Supervisor waits for Horizon to finish rather than force-killing it.

Get these right and a deploy never interrupts a job that is partway through.

The migration trap

Here is the subtle one. When you send a restart signal, workers do not all stop at once, each finishes its current job first. If your deploy also runs a migration that changes the schema, those still-running workers are now executing old code against the new database schema, which can blow up halfway through a job.

When a deploy includes a schema-changing migration, you need to stop the workers and wait for them to fully exit before migrating. supervisorctl stop blocks until the workers are down.

sudo supervisorctl stop workers:*   # blocks until all workers exit

cd /home/forge/mysite.com
git pull origin main
composer install --no-interaction --prefer-dist --optimize-autoloader
php artisan migrate --force

sudo supervisorctl start workers:*  # bring them back on new code

Now no worker is ever running old code against the migrated schema. Be aware supervisorctl stop can take a while if a long job is in flight, and the deploying user needs sudo rights for it.

Do not stop the world unless you must

Stopping and waiting for all workers adds downtime to your queue, so do not do it on every deploy. Use the plain queue:restart (or horizon:terminate) for normal deploys, and only add the supervisorctl stop and start dance when a deploy actually includes a schema change that old code cannot tolerate. Many teams keep two deploy paths: a fast default, and a "with migrations" variant for the rare breaking change.

Wrapping up

Workers run the code they booted with, so every deploy must restart them: queue:restart for plain workers, horizon:terminate for Horizon, with timeouts aligned so termination is graceful. The one extra rule is migrations: when a deploy changes the schema, stop and wait for workers with supervisorctl stop before migrating, so no worker runs old code against the new schema. Otherwise keep it simple and just restart.

More in the series: common reasons queue workers fail to restart. Questions welcome below.