When deploying a Laravel app that runs Horizon, why must you run php artisan horizon:terminate, and what does it do to workers mid-job?
answer
- long-lived workers hold old code
- SIGTERM to this host's master
- current job finishes, then exit
- process monitor starts it again
- fast_termination and --wait
basics
~20 sHorizon workers are long-running processes that booted the old code, so horizon:terminate sends SIGTERM to the local master; workers finish their current job and exit, and the process monitor restarts Horizon on the new release.
solid answer
~40 sHorizon's master, supervisors and workers boot the application once and keep it in memory, so after a deploy they keep running the previous code until restarted. `php artisan horizon:terminate` finds the master supervisors registered for this host and sends each a `SIGTERM`. The master tells its supervisors to terminate, removes its own record so a new master may start, and waits up to the longest supervisor `timeout` before exiting; each supervisor signals its workers, which finish the job in hand and then quit. A process monitor such as Supervisor then starts `php artisan horizon` again on the new code. With `fast_termination => true` in `config/horizon.php`, supervisors no longer wait for their workers unless you pass `--wait`, so the new Horizon starts while old workers drain.
code
bash · 5 lines# Horizon's step in a deploy, run on every server that runs Horizon
php artisan horizon:terminate
# with fast_termination enabled, wait for workers on this deploy only
php artisan horizon:terminate --waitgo deeper
Remember that Horizon must be terminated on each deploy and restarted by a process monitor, because workers are long-running processes.
Explain the signal path from horizon:terminate to master, supervisors and workers, and why a worker finishes its current job first.
Run it on every host, size the monitor's stop timeout for long jobs, and decide when fast_termination overlap is acceptable.
Weigh deploy speed against mixed-version processing and define what a job must tolerate when old and new workers overlap.
## Why Horizon must be restarted on deploy A PHP-FPM web request boots Laravel, handles one request and throws everything away, so new code is live on the next request. **Horizon is different.** `php artisan horizon` starts a **master supervisor** that spawns **supervisors**, which spawn **worker processes**; each boots the framework once and then loops for hours or days. Classes, config and container bindings loaded at boot stay in memory. After a deploy, those processes keep executing the **previous release** until they are restarted, and a job dispatched by the new web code may meet a worker running old job classes. That is why the Horizon docs make `php artisan horizon:terminate` a deploy step, paired with a process monitor that restarts Horizon when it exits. ## What horizon:terminate does, step by step 1. It reads the master supervisors registered in Redis and keeps those whose name starts with **this machine's host name** (the master's base name is the slug of `gethostname()`). 2. It sends each of them a `SIGTERM` with `posix_kill`, printing the process ids. 3. It stamps the framework's queue-restart cache key, the same signal `queue:restart` uses. 4. The master, on `SIGTERM`, stops its loop, tells every supervisor to terminate, and **deletes its own record** so that a new master can register immediately. 5. Each supervisor removes itself from the dashboard and sends `SIGTERM` to its workers. A Laravel worker receiving `SIGTERM` sets a quit flag: it **finishes the job it is running** and then exits instead of taking another. 6. The master waits for its supervisors, but only up to the **longest `timeout`** among them, then exits. 7. The process monitor sees `php artisan horizon` exit and starts it again, which reads the new code and the new `config/horizon.php`. ## fast_termination and --wait By default (`'fast_termination' => false`) each supervisor waits until all its workers have exited, so the deploy's restart of Horizon is delayed by the slowest job in flight. | Setting | Supervisors wait for workers? | Effect on the deploy | |---|---|---| | `fast_termination => false` (published) | yes | old and new Horizon never overlap; restart waits on long jobs | | `fast_termination => true` | no | the new Horizon can start at once while old workers finish their last job | | `fast_termination => true` plus `horizon:terminate --wait` | yes, for this run | opt back into waiting for one deploy | With fast termination, old workers and new workers briefly run side by side. That is usually fine for queue work, but it means two code versions process jobs for a short window. ## Things that go wrong - **Multi-server fleets.** The command signals only the masters named after the host it runs on, so it must run on **every** server that runs Horizon, typically as part of each server's deploy script. - **No process monitor.** `horizon:terminate` stops Horizon; without something that restarts it, the queue simply stops being worked after the deploy. - **The process monitor kills too early.** If the monitor's stop timeout is shorter than your longest job, it may kill a worker mid-job; the Horizon docs pair the Supervisor config with a generous `stopwaitsecs` for this reason. - **Config changes.** Edits to `config/horizon.php` (new supervisors, process counts) only take effect after this restart, because the provisioning plan is read when the master starts. ## Related Horizon commands | Command | What it does | |---|---| | `php artisan horizon:pause` / `horizon:continue` | stop and resume taking new jobs without exiting | | `php artisan horizon:pause-supervisor <name>` | pause one supervisor only | | `php artisan horizon:status` | report whether the master is running, paused or inactive | | `php artisan horizon:terminate` | graceful exit so the monitor restarts Horizon | Pausing is for maintenance windows; terminating is for picking up new code. ## The signals behind the commands Horizon's master and supervisors listen for POSIX signals and act on them at the next pass of their loop: - `SIGTERM` - terminate gracefully (what `horizon:terminate` sends); - `SIGUSR2` - pause: stop handing out new jobs; - `SIGCONT` - continue after a pause; - `SIGUSR1` - restart: the master terminates its supervisors so they are started again. Knowing this helps when a container platform or process monitor stops Horizon for you: a plain `SIGTERM` from the platform triggers the same graceful path as the Artisan command, provided the platform waits long enough before escalating to a hard kill.
- You deploy to three servers but run horizon:terminate from only one. What happens?Only the Horizon on that host restarts. The command filters master supervisors by this machine's host name and signals local process ids, so the other two servers keep running workers on the previous release until they are terminated on their own hosts.
- When would you turn on fast_termination?When long-running jobs make deploys wait: with `fast_termination => true`, supervisors stop waiting for their workers, so the process monitor can start a new Horizon while the old workers finish their current job. Pass `--wait` on a deploy where overlap between two code versions is not acceptable.
saying these in an interview costs you the question
- Horizon workers pick up new code on their next job automatically
- horizon:terminate kills workers immediately, abandoning jobs in progress
- One horizon:terminate call restarts Horizon on every server in the fleet
- horizon:terminate restarts Horizon by itself without a process monitor
- horizon:pause is the deploy step that loads the new release