To restart Laravel Horizon during application deployments, run the Artisan command php artisan horizon:terminate inside your release pipeline. This command instructs the master supervisor process to instruct all worker child processes to finish their current job, exit cleanly, and allow an external process monitor like Supervisor or systemd to spawn a fresh Horizon process tree with your newly deployed code.
Most deployment runbooks treat php artisan horizon:terminate as a magic wand, yet relying on it blindly without understanding signal propagation is an operational anti-pattern. Engineers often assume that issuing a restart immediately guarantees fresh code execution across background jobs. In reality, terminating Horizon without configuring process manager timeouts, job payload serialization safety, and Redis eviction locks introduces subtle race conditions that corrupt transaction state, create zombie worker processes, and waste thousands of engineering hours debugging ghost failures.
Graceful process lifecycle management requires balancing worker eviction against active execution deadlines. When background queues handle mission-critical financial ledger updates, multi-tenant billing jobs, or large batch exports, a naive restart script can instantly degrade infrastructure stability. Managing Horizon effectively demands treating background workers as stateful execution runtimes governed by explicit process supervisory trees.
Process Architecture and the Horizon Master Supervisor
To understand why Horizon requires deliberate orchestration during deployments, one must examine its underlying operational model. Laravel Horizon is not merely a Redis queue consumer; it is an intelligent, multi-layered process orchestration system written entirely in PHP. The master Horizon process acts as a top-level supervisory monitor that manages one or more intermediate pool supervisors, which in turn spawn and monitor individual worker processes (the queue:work instances).
When you start Horizon via php artisan horizon, the runtime initializes a tree structure:
- Master Process: Coordinates overall configuration, listens for operating system signals, records system metrics into Redis, and manages pool configurations.
- Pool Supervisors: Track specific queue configurations, enforce concurrency policies, and calculate auto-scaling metrics based on queue workload.
- Worker Processes: Ephemeral child processes dedicated to pulling job payloads off Redis lists, executing PHP business logic, and releasing memory upon job completion.
Because PHP CLI applications cache compiled byte-code in memory and initialize framework state once per process lifecycle, long-running worker processes do not automatically pick up changed code files on disk. If your deployment script modifies a service class or an Eloquent model, running workers continue executing the memory-resident version of the previous code. Orchestrating a restart is mandatory to invalidate this in-memory application context.
The Horizon Terminate Command vs Worker Restart
A common source of confusion in queue operations is the difference between php artisan queue:restart and php artisan horizon:terminate. While both commands utilize the Redis cache layer to broadcast lifecycle instructions, their operational scope is completely distinct.
The traditional queue:restart command updates a timestamp in your default cache store. Every active queue:work loop checks this timestamp against its own boot time after processing a job. If the cache timestamp is newer, the worker exits. However, Horizon runs its own supervisory hierarchy that bypasses parts of the standard queue daemon lifecycle. If you execute queue:restart on an instance managed by Horizon, the internal supervisor immediately catches the worker exit and restarts a new child process under the old Horizon master configuration.
Conversely, horizon:terminate informs the master Horizon supervisor process itself to shut down entirely:
// Inside Laravel Horizon's TerminateCommand execution path
$this->laravel['events']->dispatch(new MasterSupervisorTerminated);
// Horizon writes a termination flag to Redis monitored by the master loop
$this->laravel->make(MasterSupervisorRepository:class)->terminate();
The master supervisor receives this signal, stops reading new jobs from Redis, broadcasts a soft termination instruction down to all child supervisors, waits for active workers to complete their discrete tasks, and finally exits with status code 0. For continuous queue processing, an external daemon supervisor must wrap Horizon to detect this exit and re-launch the master binary.
POSIX Signals and Graceful Shutdown Mechanics
Under the hood, graceful worker termination relies on POSIX signal handling within the PHP runtime. When Horizon is instructed to terminate, it sends a SIGTERM signal down the process tree. Handling this signal cleanly requires an operating system environment that supports asynchronous signal handling through the pcntl PHP extension.
Understanding how Horizon maps signals to process lifecycles is essential for setting operational timeouts:
| Signal | PHP Trait / Listener | Horizon Master Action | Child Worker Action |
|---|---|---|---|
SIGTERM |
ListensForSignals |
Terminates pool supervisors cleanly and exits. | Finishes active job, skips next payload, exits. |
SIGINT |
ListensForSignals |
Initiates immediate termination sequence. | Gracefully stops execution loop. |
SIGCONT |
ListensForSignals |
Instructs supervisors to resume paused queues. | Resumes popping jobs from Redis. |
SIGKILL |
Uncatchable (OS Kernel) | Immediate abnormal abort; leaves Redis locks orphan. | Kernel forcibly drops process memory space immediately. |
When a worker process receives SIGTERM, it does not abort mid-statement. It completes the active execution cycle of the job currently assigned to it, writes its completion state to Redis, and exits. If a single job takes 120 seconds to run and your deployment pipeline abruptly fires a SIGKILL after 30 seconds, data corruption occurs. Engineering teams managing full cycle software development services must calibrate process orchestrators to allow sufficient drain windows before terminating execution nodes.
Configuring Supervisor for Horizon Auto-Recovery
Because php artisan horizon:terminate commands the master process to exit, Horizon requires an external operating system supervisor to relaunch the daemon. The standard Linux production daemon is Supervisor (supervisord). A naive Supervisor configuration can easily cause deployment deadlocks or unmonitored drop-offs.
Below is a production-grade Supervisor configuration file for Horizon, optimized for modern high-throughput environments:
[program:laravel-horizon]
process_name=%(program_name)s
command=php /var/www/app/current/artisan horizon
autostart=true
autorestart=true
user=forge
redirect_stderr=true
stdout_logfile=/var/log/supervisor/horizon.log
stopwaitsecs=3600
stopsignal=SIGTERM
priority=999
Pay strict attention to the stopwaitsecs parameter. By default, Supervisor configures stopwaitsecs=10. If an active job (such as a large PDF generation or bank reconciliation) takes 45 seconds to finish, Supervisor will send SIGTERM, wait 10 seconds, and then issue a violent SIGKILL. Setting stopwaitsecs to a value higher than your longest possible queue timeout (for example, 3600 seconds for hourly batch jobs) guarantees that supervisord never kills an active worker mid-flight during automated redeployments.
Systemd Service Units for Modern Linux Stacks
Many modern cloud installations are deprecating supervisord in favor of native systemd service units to reduce system footprint and unify logging under journald. Systemd provides robust process supervision and can manage Horizon directly with built-in restart mechanics.
Create a dedicated unit file at /etc/systemd/system/laravel-horizon.service:
[Unit]
Description=Laravel Horizon Process Manager
After=network.target redis-server.service
Requires=redis-server.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/app/current
ExecStart=/usr/bin/php /var/www/app/current/artisan horizon
Restart=always
RestartSec=2s
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=3600
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
The critical directive here is KillMode=mixed combined with TimeoutStopSec=3600. When systemd initiates a stop or restart, it sends SIGTERM only to the master Horizon process (the root process of the cgroup), allowing Horizon to manage the staged shutdown of its pool supervisors and workers. Only when the TimeoutStopSec timer expires without a clean exit will systemd broadcast SIGKILL to all remaining child processes inside the control group.
Zero-Downtime Deployment Integration via CI/CD
Achieving zero downtime requires strict synchronization between your symlink atomic swap and the Horizon termination command. If you terminate Horizon before switching the symbolic release link, Horizon spawns new workers against the old code directory. If you switch the symlink but fail to terminate Horizon, workers process new jobs using stale memory structures, frequently breaking due to schema mismatches.
Deployer, Envoy, and GitHub Actions pipelines should follow this precise ordering:
- Build: Install dependencies, compile frontend assets, and prepare database migrations.
- Activate: Point the live release symlink to the new deployment folder:
ln -sfn /var/www/releases/42 /var/www/current. - Reload FPM: Gracefully reload PHP-FPM to ensure synchronous web traffic utilizes new byte-code.
- Signal Horizon: Issue
php artisan horizon:terminatefrom the newly activated release path.
Executing horizon:terminate from the new directory ensures the Artisan runner interacts with the current project environment. As the old master process detects the termination signal and winds down, Supervisor or systemd executes php /var/www/current/artisan horizon, reading the fresh code release instantly. While handling complex database updates alongside queue changes, engineers must also consider Laravel ORM architecture and optimization to prevent lock contention between transactional migrations and pending worker jobs.
Containerized Horizon: Docker and Kubernetes Lifecycles
In containerized environments, the traditional model of relying on supervisord inside the container to cycle Horizon is an architectural smell. Containers should follow the single-concern principle: one process per container. In Docker or Kubernetes, Horizon should run as the foreground process (PID 1) of a dedicated worker pod or container.
Consider this standard Dockerfile entrypoint pattern:
# Container execution pattern
ENTRYPOINT ["php", "artisan", "horizon"]
In this architecture, you do not execute horizon:terminate inside the running container. Instead, you trigger a rolling deployment in your orchestrator:
- In Kubernetes, updating the Deployment manifest triggers a rolling update. Kubernetes sends a
SIGTERMto the old pod. - Horizon catches the
SIGTERMnatively and begins its graceful drain routine. - The pod status moves to
Terminating, and Kubernetes removes the pod from endpoints while respecting theterminationGracePeriodSecondsdirective in your pod specification. - A new pod running the updated container image is scheduled concurrently, ensuring zero interruption in job processing.
If your terminationGracePeriodSeconds is configured to the default 30 seconds, long jobs will be terminated violently. Ensure this value is increased in your deployment YAML to accommodate your maximum workload duration.
Edge Cases: Zombie Workers, Long-Running Jobs, and Memory Bloat
Even well-configured platforms encounter edge cases where workers refuse to terminate or master supervisors fail to cycle cleanly. The root cause typically falls into one of three structural failure categories:
1. Blocking I/O System Calls
PHP signal handlers dispatched via pcntl_signal_dispatch() cannot interrupt blocking C-level system calls. If a queue worker makes an external HTTP request or a slow socket query without a strict connection timeout, the PHP process hangs inside the kernel network socket read. The worker process cannot process the queued SIGTERM until the network socket closes or times out. Always enforce strict cURL and stream timeouts across all outbound HTTP integrations.
2. Orphaned Zombie Children
If the Horizon master supervisor crashes due to an out-of-memory error, its child worker processes can become reparented to system PID 1. These orphaned workers will continue pulling jobs from Redis indefinitely, running outdated application logic. To clear orphaned workers manually, identify and terminate the running processes:
# Find orphaned horizon queue workers
ps aux | grep 'horizon:work' | grep -v grep
# Safely send SIGTERM to all running queue workers
pkill -f 'horizon:work'
3. Memory Leaks in Batch Loops
Horizon workers run continuously until an exit condition is met. While Laravel provides the --memory option to terminate workers exceeding specific thresholds, developers often introduce memory leaks by accumulating models in local static arrays or failing to disable query logging during data migrations. Ensure DB:disableQueryLog() is invoked within long-running batch iterations.
Health Checks, Metrics, and Failure Detection
Verifying that a restart succeeded requires programmatic monitoring beyond merely asserting that an Artisan command exited with return code 0. Horizon writes operational state into Redis, which can be queried to confirm runtime health.
The built-in status check can be incorporated into post-deployment automated health probes:
# Assert that Horizon master is actively running
php artisan horizon:status | grep -q 'Horizon is running'
if [ $? -ne 0 ]; then
echo "Horizon failed to initialize after deployment" >&2
exit 1
fi
Additionally, monitoring tools should watch Redis keys directly. Horizon writes heartbeats to a dedicated hash set: horizon:master:*. If the heartbeat timestamp drifts beyond your configured threshold (typically 180 seconds), alert on process starvation. Automated synthetic probes should regularly dispatch dummy ping jobs through your low-priority queue to verify round-trip execution latency.
Financial Investment and Operational Cost Models
Managing queue processing infrastructure, process supervisors, and deployment automation carries direct infrastructure and human resource costs. Organizations typically evaluate three primary models for developing and maintaining high-availability Laravel queue infrastructures:
| Engagement Model | Initial Architecture Cost | Monthly Ongoing Retainer | Average Hourly Rate | Risk & TCO Factors |
|---|---|---|---|---|
| Internal Platform Team | $15,000 to $35,000 | $8,000 to $18,000 | $85 to $145 / hr | High fixed overhead, internal expertise retention risks. |
| Specialized Agency / Consultancy | $8,000 to $20,000 | $3,500 to $7,500 | $150 to $220 / hr | Lower long-term TCO, guaranteed SLAs, multi-stack expertise. |
| Managed Platform (PaaS / Cloud) | $2,500 to $6,000 | $500 to $2,500 | $120 to $180 / hr (contract support) | Vendor lock-in, limited control over deep kernel signal tuning. |
Engineering leadership must weigh setup speed against long-term maintenance costs. While a fully internal platform implementation requires significant upfront architectural investment, it provides complete ownership of runtime internals. Conversely, relying on contract infrastructure retainers ($3,500 to $7,500 monthly) shields the team from unexpected operational overhead during framework version updates.
Architectural Directory Reference
Managing background queues effectively is only one piece of a reliable application platform. For deeper architectural standards, foundational framework paradigms, and core configuration strategies, explore our comprehensive technical catalog.
Explore our complete Laravel, Basics directory for more guides.
Factors That Affect Development Cost
- Custom process supervisor orchestration (systemd vs supervisord)
- High availability Redis cluster architecture
- Zero-downtime continuous deployment pipeline automation
- Third-party monitoring and queue health alerting setups
Total implementation and support costs vary depending on queue volume, containerization requirements, and internal infrastructure capabilities.
Orchestrating a predictable laravel horizon restart is fundamentally an exercise in process tree lifecycle management. By moving away from blind Artisan command invocations and adopting explicit operating system signal boundaries, engineering teams eliminate race conditions, prevent deployment-induced data corruption, and safeguard long-running background tasks. Whether using supervisord, systemd, or native Kubernetes orchestrators, configuring explicit shutdown grace periods ensures that your background workers drain cleanly before code switches.
As queue infrastructure grows in complexity and throughput, continuous monitoring of worker health and Redis process heartbeats becomes vital. Implementing robust zero-downtime deployment pipelines that isolate symlink updates from process termination ensures that high-volume platforms maintain high availability without manual operational firefighting.