Updated June 2026. Tested on Laravel 13, PHP 8.4 and Horizon 5. Part of the Techalyst queue series. See also How Laravel Horizon Works and Laravel queues and deployments.

One of Horizon's nicest features is graceful shutdown: when told to stop, it waits for any in-flight jobs to finish before terminating its worker processes, so nothing is cut off halfway. You trigger it on deploy with php artisan horizon:terminate. But "graceful" is only guaranteed if three timeout values line up. Get one wrong and a deploy can still kill a job in the middle.

The three timeouts that must agree

For Horizon to truly wait for your jobs, each layer above a job must be willing to wait longer than the job itself.

  • Your job's timeout must be shorter than the Horizon supervisor's timeout. A job should always be the first thing to time out, never the layer above it.
  • The Horizon supervisor's timeout must be greater than your longest running job. This is what makes the supervisor wait for that job to finish instead of force-killing it.
  • Supervisor's stopwaitsecs (if the system process manager Supervisor monitors your Horizon process) must be greater than your longest running job, so Supervisor waits for Horizon to terminate on its own rather than force-killing the whole Horizon process.

Picture it as nested patience: the job finishes first, the Horizon supervisor is willing to wait longer than the job, and the system Supervisor is willing to wait longer still. As long as each outer layer out-waits the one inside it, no job is ever interrupted.

What each setting looks like

The job and supervisor timeouts live in your Horizon config.

// config/horizon.php
'environments' => [
    'production' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue'      => ['default'],
            'balance'    => 'auto',
            'timeout'    => 300, // longer than your longest job
        ],
    ],
],
// the job itself
class GenerateLargeReport implements ShouldQueue
{
    public $timeout = 280; // shorter than the supervisor's 300
}

And stopwaitsecs lives in the system Supervisor program block that runs Horizon.

[program:horizon]
command=php /home/forge/site.com/artisan horizon
stopwaitsecs=360   ; longer than your longest job, so Supervisor waits for Horizon

With these three in the right order (job timeout < horizon supervisor timeout < stopwaitsecs), a horizon:terminate on deploy lets every running job finish, lets Horizon shut down cleanly, and only then does the process manager start the fresh Horizon on your new code.

Why this matters on every deploy

Workers run the code they booted with, so each deploy must restart Horizon, and the command for that is horizon:terminate. If the timeouts are misaligned, that very command, meant to be graceful, becomes the thing that force-kills a job partway through, which can leave half-finished work and corrupt state. Lining up the three timeouts is what makes your deploys safe for long running jobs.

Wrapping up

Horizon shuts down gracefully by waiting for in-flight jobs, but only when three timeouts agree: the job timeout is the smallest, the Horizon supervisor timeout is larger, and Supervisor's stopwaitsecs is larger still, each comfortably above your longest running job. Set them in that order and horizon:terminate on deploy will never interrupt a job mid run.

More in the series: How Laravel Horizon works and Laravel queues and deployments. Questions welcome below.