Laravel Horizon graceful shutdowns are critical for reliable background processing. Learn to implement signal handling to prevent data loss in high-concurrency.

When you’re pushing code at scale, the last thing you want is a deployment that kills your background workers mid-job. I spent an entire on-call rotation chasing down "zombie" database records because our workers were getting a SIGKILL instead of finishing their work. If you're running heavy background tasks, you need to master how Laravel Horizon interacts with the underlying PHP process management layer.
By default, when you restart a service or scale down your fleet, the process manager sends a SIGTERM signal. If your worker isn't listening, it shuts down instantly. Any job currently being processed is effectively aborted, often leaving your application in an inconsistent state.
When we first hit this issue, we were blindly relying on the default Supervisor configuration. We saw roughly 15% of our long-running report-generation jobs failing during deployments. We had to implement custom signal handling to ensure that once a job starts, it finishes before the process exits.
Horizon is built on top of the pcntl extension. If you don't have this installed, stop reading and install it immediately. It’s the backbone of how your PHP processes communicate with the OS.
When Horizon receives a SIGTERM (which happens during a php artisan horizon:terminate or a standard service restart), it doesn't kill the child processes immediately. Instead, it sets a flag to stop picking up new jobs. The worker finishes its current task, checks the signal state, and then gracefully exits.
However, the "graceful" part only works if your code doesn't block the signal.
To ensure your workers respect these signals, you need to be aware of long-blocking calls. If you have a job that performs a 30-second API request, and your timeout is set to 60 seconds, the worker might try to exit while the request is still pending.
Here is how I structure my job classes to be "signal-aware" in high-concurrency environments:
PHPpublic function handle() { #6A9955">// Break heavy loops into smaller chunks foreach ($this->largeDataSet as $chunk) { if ($this->shouldQuit()) { $this->release(10); #6A9955">// Put it back in the queue return; } $this->process($chunk); } } protected function shouldQuit(): bool { return extension_loaded('pcntl') && pcntl_signal_dispatch() && $this->isShuttingDown(); }
Wait, don't over-engineer this. Laravel’s base Job class already includes a jobShouldBeReleased() check if you're using the InteractsWithQueue trait. The key is ensuring your job isn't doing something that ignores the signal, like a massive sleep() or a poorly configured curl timeout.
If you're deploying on Kubernetes, your setup is likely more complex than a standard VPS. When Scaling Laravel Queues on Kubernetes: A KEDA Implementation Guide, the pod termination grace period is your best friend.
If your Kubernetes terminationGracePeriodSeconds is shorter than the time it takes for your longest job to finish, you're fighting a losing battle. Even if your code is perfect, the orchestrator will pull the plug.
horizon.php configuration: Ensure the timeout is long enough.pcntl: Run php -m | grep pcntl on your worker nodes.We once tried to use a custom signal handler in a ServiceProvider to catch SIGTERM and manually flush our Redis cache. It broke everything. The problem with manual signal handling in PHP is that it’s easy to accidentally overwrite the internal handlers that Laravel uses to manage the worker state.
Stick to the framework's built-in InteractsWithQueue mechanisms. If you need to perform cleanup, use the failed() method or the JobProcessed event. Don't try to intercept the signal at the process level unless you're writing a custom binary extension.
Q: Should I use pcntl_async_signals(true) in my jobs?
A: Usually, no. Laravel handles this internally. Enabling it manually can lead to race conditions where the worker processes a signal while it's in the middle of a database transaction, leading to corrupted state.
Q: How do I know if my workers are actually shutting down gracefully? A: Check your logs during a deployment. You should see "Worker exiting" messages. If you see "Worker killed" or "Process terminated by signal 9," your workers are being force-killed by the OS before they finish.
Q: Does Horizon handle SIGINT the same as SIGTERM?
A: Yes, Horizon treats them similarly to initiate a graceful stop. If you're using Running background workers with systemd for production reliability, ensure your KillSignal is set to SIGTERM so systemd gives Horizon the chance to perform its shutdown routine.
The biggest takeaway after years of running these workers: keep your jobs idempotent. No matter how well you implement Laravel Horizon signal handling, network partitions or hardware failures happen. If a job can safely be restarted from the beginning, you stop worrying about whether it was interrupted by a SIGTERM or a server crash.
I'm still experimenting with how to handle "zombie" jobs that hang indefinitely due to external API timeouts. For now, strict timeout settings in the horizon.php config file remain the most effective guardrail.
Optimize Laravel Eloquent performance by leveraging PostgreSQL generated columns. Learn to move complex transformations to the DB for faster read models.
Read more