If you've ever deployed a Laravel application mid-traffic and watched jobs vanish from your queue, you know the pain. The default setup — a basic php artisan queue:work process with a simple restart hook — is fine for low-traffic apps. But once you're processing thousands of jobs an hour, a naive deploy becomes a liability.

In this post I'll walk through the exact setup I use on production: Laravel Horizon for queue management, Supervisor for process supervision, and Envoyer for atomic deployments with zero-downtime worker restarts.

The Problem with Naive Restarts

When you run php artisan horizon:terminate during a deploy, Horizon signals all workers to finish their current job and exit cleanly. That's good. The problem is the gap between termination and Supervisor restarting the process. During that window — even if it's only a few seconds — jobs are silently delayed or, if you're using --stop-when-empty incorrectly, dropped entirely.

Key insight: The goal isn't to restart workers instantly — it's to ensure no job starts processing against stale code, and no job is abandoned mid-flight.

Supervisor Configuration

Start with a solid Supervisor config. The stopwaitsecs value here is critical — it must be longer than your longest-running job, otherwise Supervisor will SIGKILL a running worker.

[program:horizon]
process_name=%(program_name)s
command=php /var/www/html/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/horizon.log
stopwaitsecs=3600
stopsignal=SIGTERM

Envoyer Deployment Hook

In your Envoyer project, add an After Activate hook that signals Horizon to terminate gracefully. The new release is already in place, so when Supervisor restarts Horizon, it picks up fresh code automatically.

php {{ release }}/artisan horizon:terminate

This is the entire hook. The order matters: Envoyer activates the new release (symlink swap), then terminates old workers. Workers running against the old release finish their jobs cleanly. New workers start against the new code.

Monitoring the Transition

In your Horizon dashboard, watch the Processes tab during a deploy. You should see the old process count dropping as workers finish jobs, and the new process count climbing as Supervisor restarts them. Total processing capacity dips momentarily but never hits zero.

Verification checklist

  • No failed jobs during the deploy window
  • Supervisor log shows clean SIGTERM and restart sequence
  • Horizon dashboard shows correct worker count after 60 seconds
  • No "stale" worker processes running against old release

This setup has served well across several high-volume Laravel applications. The 60-line Supervisor config and single Envoyer hook is all you need for true zero-downtime queue deploys.