Laravel Horizon Tuning &amp; Reliable Job Throughput | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Laravel Queues: Horizon Metrics, Supervisor Tuning, and Reliable Job Throughput

 Laravel Queues: Horizon Metrics, Supervisor Tuning, and Reliable Job Throughput
================================================================================

 Go beyond basic queue setup. Learn how to tune Horizon balancing strategies, configure Supervisor for zero-downtime deploys, and build observable, reliable job pipelines in production Laravel applications.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 30 Jun 2026 · Updated 30 Jun 2026 · 3 min read

ShareCopy linkCopied

 ![Laravel Queues: Horizon Metrics, Supervisor Tuning, and Reliable Job Throughput](https://cdn.msaied.com/329/b8b8a1253f218f9cae9c423ea28ec877.png) 

  On this page +1. [Beyond php artisan queue:work: Running Queues Seriously in Production](#beyond-codephp-artisan-queueworkcode-running-queues-seriously-in-production)
2. [Horizon Balancing Strategies](#horizon-balancing-strategies)
3. [Supervisor: Zero-Downtime Deploys](#supervisor-zero-downtime-deploys)
4. [Job Timeouts and the SIGKILL Problem](#job-timeouts-and-the-codesigkillcode-problem)
5. [Observability: Metrics That Matter](#observability-metrics-that-matter)
6. [Practical Takeaways](#practical-takeaways)

 Beyond `php artisan queue:work`: Running Queues Seriously in Production
-----------------------------------------------------------------------

Most Laravel developers know how to push a job and run a worker. Far fewer have thought carefully about *how many* workers to run, *which* balancing strategy to use, or *what happens* when a deploy kills a long-running job mid-execution. This article covers the practical decisions that separate a fragile queue setup from a production-grade one.

---

Horizon Balancing Strategies
----------------------------

Horizon ships with three balancing strategies: `simple`, `auto`, and `false` (manual). The choice has real consequences.

```php
// config/horizon.php
'environments' => [
    'production' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue' => ['critical', 'default', 'low'],
            'balance' => 'auto',
            'minProcesses' => 2,
            'maxProcesses' => 20,
            'balanceMaxShift' => 3,
            'balanceCooldown' => 3,
            'tries' => 3,
            'timeout' => 90,
        ],
    ],
],

```

**`auto`** measures throughput per queue and redistributes workers every `balanceCooldown` seconds, shifting at most `balanceMaxShift` processes at a time. This is the right default for mixed-priority workloads.

**`simple`** divides workers evenly across queues — predictable but blind to actual queue depth.

Keep `balanceMaxShift` low (2–3) to avoid thrashing. A sudden spike that shifts 10 workers at once starves other queues for the cooldown window.

---

Supervisor: Zero-Downtime Deploys
---------------------------------

Horizon itself is managed by Supervisor. The critical piece most teams miss is graceful termination on deploy.

```ini
; /etc/supervisor/conf.d/horizon.conf
[program:horizon]
process_name=%(program_name)s
command=php /var/www/artisan horizon
autostart=true
autorestart=true
stopwaitsecs=3600
stopsignal=SIGTERM
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/horizon.log

```

`stopwaitsecs=3600` tells Supervisor to wait up to one hour for Horizon to finish in-flight jobs before force-killing. Without this, a deploy during a long job causes a silent partial execution.

In your deploy script:

```bash
php artisan horizon:terminate
sudo supervisorctl restart horizon

```

`horizon:terminate` signals Horizon to stop accepting new jobs and wait for current workers to finish. Supervisor then restarts the process cleanly with the new code.

---

Job Timeouts and the `SIGKILL` Problem
--------------------------------------

PHP's `pcntl` extension allows Horizon to send `SIGTERM` to a worker when its `timeout` is exceeded. The worker can catch this and clean up. But if `timeout` in `horizon.php` is *shorter* than the job's own `$timeout` property, the worker is killed before the job can handle it.

```php
class GenerateReport implements ShouldQueue
{
    public int $timeout = 120; // job-level
    public int $tries = 2;

    public function handle(): void
    {
        // long-running work
    }

    public function failed(Throwable $e): void
    {
        // cleanup, notify, etc.
    }
}

```

Rule: the Horizon supervisor `timeout` should always be slightly *higher* than the longest job `$timeout` in that queue. A 10-second buffer is sufficient.

---

Observability: Metrics That Matter
----------------------------------

Horizon exposes a `/horizon/api/stats` endpoint and stores metrics in Redis. Expose these to your monitoring stack:

```php
// In a scheduled command or Prometheus exporter
$stats = app(\Laravel\Horizon\Contracts\MetricsRepository::class);

$throughput = $stats->throughput(); // jobs/min across all queues
$runtime = $stats->runtimeForQueue('default'); // avg ms
$failRate = $stats->failedJobsPerMinute();

```

Alert on:

- Queue depth growing faster than throughput (workers undersized)
- Average runtime increasing over time (memory leak or slow dependency)
- Failed jobs per minute spiking (upstream service degradation)

---

Practical Takeaways
-------------------

- Use `auto` balancing with conservative `balanceMaxShift` and `balanceCooldown` values.
- Set `stopwaitsecs` in Supervisor to at least your longest expected job runtime.
- Always call `horizon:terminate` before restarting — never `supervisorctl restart` cold.
- Keep Horizon supervisor `timeout` slightly above the job-level `$timeout`.
- Instrument throughput, runtime, and failure rate; queue depth alone is not enough.
- Separate high-priority work into a dedicated `critical` queue with its own supervisor block.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [queues](https://www.msaied.com/public/articles?search=queues)
- [horizon](https://www.msaied.com/public/articles?search=horizon)
- [devops](https://www.msaied.com/public/articles?search=devops)
- [performance](https://www.msaied.com/public/articles?search=performance)

 Frequently asked questions 
---------------------------

  What is the difference between Horizon's `auto` and `simple` balancing strategies?`auto` dynamically redistributes workers based on measured queue throughput, shifting processes toward busier queues within the configured cooldown window. `simple` divides workers evenly across all queues regardless of actual depth, which is predictable but inefficient under uneven load.

   Why does `stopwaitsecs` in Supervisor matter for Laravel Horizon?Without a sufficient `stopwaitsecs`, Supervisor force-kills the Horizon process before in-flight jobs finish when you restart during a deploy. Setting it to at least your longest expected job duration gives `horizon:terminate` time to drain workers gracefully.

   How do I prevent jobs from being silently killed mid-execution on deploy?Run `php artisan horizon:terminate` in your deploy script before restarting Supervisor. This signals Horizon to stop accepting new jobs and wait for current workers to complete, then Supervisor restarts the process with fresh code.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articlePostgreSQL Full-Text Search in Laravel: Indexes, Ranking, and Multilingual Queries](https://www.msaied.com/public/articles/postgresql-full-text-search-in-laravel-indexes-ranking-and-multilingual-queries-1) [Next articleLaravel Concurrency Facade and Process Pools for Parallel Work](https://www.msaied.com/public/articles/laravel-concurrency-facade-and-process-pools-for-parallel-work-2)  

   On this page
-------------

1. [Beyond php artisan queue:work: Running Queues Seriously in Production](#beyond-codephp-artisan-queueworkcode-running-queues-seriously-in-production)
2. [Horizon Balancing Strategies](#horizon-balancing-strategies)
3. [Supervisor: Zero-Downtime Deploys](#supervisor-zero-downtime-deploys)
4. [Job Timeouts and the SIGKILL Problem](#job-timeouts-and-the-codesigkillcode-problem)
5. [Observability: Metrics That Matter](#observability-metrics-that-matter)
6. [Practical Takeaways](#practical-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/743/8998fac3a41451ab3fe1588194e17a43.png) Filament · 3 min read### Securing Filament Plugins with Plumb: Automated Security Scoring for PHP Packages

5 Oct 2026 ](https://www.msaied.com/public/articles/securing-filament-plugins-with-plumb-automated-security-scoring-for-php-packages) [ ![](https://cdn.msaied.com/742/2d02018669cdeedccb5de2efb898f0ee.png) Filament · 3 min read### Filament v3.3.56 Released: File Hash Names and Livewire Upload Fix

5 Oct 2026 ](https://www.msaied.com/public/articles/filament-v3356-released-file-hash-names-and-livewire-upload-fix) [ ![](https://cdn.msaied.com/741/5b55c123ad08e4d34e1f4b99ad6a428b.png)  · 3 min read### Filament v4 Schema-Based Forms, Infolists, and the Unified Schema API

5 Oct 2026 ](https://www.msaied.com/public/articles/filament-v4-schema-based-forms-infolists-and-the-unified-schema-api-5) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
