Laravel Horizon Tuning &amp; Production Scaling | 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 Horizon: Queue Metrics, Supervisor Tuning, and Graceful Scaling in Production

 Laravel Horizon: Queue Metrics, Supervisor Tuning, and Graceful Scaling in Production
======================================================================================

 Beyond the dashboard: how to tune Horizon supervisors, interpret queue metrics, handle long-running jobs safely, and scale workers without dropping messages in production Laravel applications.

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

ShareCopy linkCopied

 ![Laravel Horizon: Queue Metrics, Supervisor Tuning, and Graceful Scaling in Production](https://cdn.msaied.com/390/25668fca0fa0cd1acbd22f27230b8ef5.png) 

  On this page +1. [Laravel Horizon in Production: Beyond the Pretty Dashboard](#laravel-horizon-in-production-beyond-the-pretty-dashboard)
2. [How Horizon Supervisors Actually Work](#how-horizon-supervisors-actually-work)
3. [Choosing the Right Balance Strategy](#choosing-the-right-balance-strategy)
4. [Reading Horizon Metrics Programmatically](#reading-horizon-metrics-programmatically)
5. [Graceful Termination and Long-Running Jobs](#graceful-termination-and-long-running-jobs)
6. [Memory Leaks and maxJobs](#memory-leaks-and-codemaxjobscode)
7. [Takeaways](#takeaways)

 Laravel Horizon in Production: Beyond the Pretty Dashboard
----------------------------------------------------------

Horizon ships with a slick UI, but most teams stop there. The real value is in its supervisor model, metric snapshots, and the knobs that determine whether your queue survives a traffic spike or silently drops work.

### How Horizon Supervisors Actually Work

Horizon manages one or more *supervisor* processes, each of which spawns a pool of `queue:work` child processes. The key insight: **supervisors are not queues**. A single supervisor can watch multiple queues with a priority order.

```php
// config/horizon.php
'environments' => [
    'production' => [
        'supervisor-default' => [
            'connection' => 'redis',
            'queue' => ['critical', 'default', 'low'],
            'balance' => 'auto',
            'autoScalingStrategy' => 'time',
            'maxProcesses' => 20,
            'minProcesses' => 2,
            'maxTime' => 0,
            'maxJobs' => 500,
            'memory' => 256,
            'tries' => 3,
            'timeout' => 90,
            'nice' => 0,
        ],
    ],
],

```

`balance => 'auto'` tells Horizon to redistribute workers across queues based on wait time. `autoScalingStrategy => 'time'` scales toward minimising wait time rather than raw queue size — better for latency-sensitive work.

### Choosing the Right Balance Strategy

| Strategy | Scales on | Best for | |---|---|---| | `simple` | Queue size ratio | Predictable, uniform jobs | | `auto` | Wait time | Mixed job durations | | `false` | Fixed processes | Isolated, critical queues |

For a SaaS with a `critical` queue (password resets, webhooks) and a `low` queue (report generation), use **two supervisors** with `balance => false` on critical and `auto` on low:

```php
'supervisor-critical' => [
    'queue' => ['critical'],
    'balance' => false,
    'minProcesses' => 3,
    'maxProcesses' => 3,
],
'supervisor-bulk' => [
    'queue' => ['default', 'low'],
    'balance' => 'auto',
    'minProcesses' => 1,
    'maxProcesses' => 15,
    'autoScalingStrategy' => 'time',
],

```

This guarantees critical work always has dedicated workers, while bulk work scales elastically.

### Reading Horizon Metrics Programmatically

Horizon stores throughput and runtime snapshots in Redis. You can query them directly:

```php
use Laravel\Horizon\Contracts\MetricsRepository;

$metrics = app(MetricsRepository::class);

$throughput = $metrics->throughputForQueue('default'); // jobs/min
$runtime = $metrics->runtimeForQueue('default');       // ms average

```

Pipe these into your observability stack (Datadog, Prometheus via a custom exporter) rather than relying solely on the Horizon UI. Alert when `throughputForQueue` drops to zero during business hours — that's a dead worker, not an empty queue.

### Graceful Termination and Long-Running Jobs

Horizon sends `SIGTERM` to workers during deploys. Workers finish their current job, then exit. The danger: a job that runs for 180 seconds will block the deploy for 180 seconds per worker.

Mitigate with `maxTime` (seconds a worker runs before self-terminating after its current job) and by designing long jobs to be interruptible:

```php
public function handle(): void
{
    foreach ($this->chunks() as $chunk) {
        if ($this->job->isDeleted() || $this->job->isReleased()) {
            return;
        }

        $this->processChunk($chunk);

        // Respect the terminate signal between chunks
        if (app('queue.worker')->shouldQuit) {
            $this->release(5);
            return;
        }
    }
}

```

Accessing `app('queue.worker')->shouldQuit` lets a job voluntarily release itself back to the queue when a shutdown is pending, preventing message loss.

### Memory Leaks and `maxJobs`

PHP workers accumulate memory over time — Eloquent model event listeners, static caches, and package bootstrapping all contribute. `maxJobs => 500` forces a worker restart after 500 jobs, which is a pragmatic ceiling. Pair it with `memory => 256` (MB) as a hard cap:

```php
'maxJobs' => 500,
'memory' => 256,

```

Monitor actual worker memory with `horizon:snapshot` on a schedule:

```php
// app/Console/Kernel.php
$schedule->command('horizon:snapshot')->everyFiveMinutes();

```

Without snapshots, the Horizon metrics graphs are empty.

### Takeaways

- Use **two supervisors** to isolate critical queues from bulk work; never mix them with `auto` balance.
- Set `autoScalingStrategy => 'time'` for latency-sensitive queues; `size` can over-provision on bursty workloads.
- Export Horizon metrics to your APM — don't rely on the UI for alerting.
- Design long jobs to check `shouldQuit` between chunks so deploys don't stall.
- Schedule `horizon:snapshot` every five minutes or your throughput graphs will be blank.

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

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

  What is the difference between `balance =&gt; 'auto'` and `balance =&gt; 'simple'` in Horizon?`simple` distributes workers proportionally to queue size. `auto` targets wait time, so it shifts workers toward queues where jobs are waiting longest regardless of raw count — better when jobs have variable durations.

   How do I prevent a long-running job from blocking a Horizon deploy?Check `app('queue.worker')-&gt;shouldQuit` between processing units inside your job. When it is true, call `$this-&gt;release()` to put the job back on the queue and return early, allowing the worker to exit cleanly.

   Why are my Horizon throughput graphs empty?Horizon only records metric snapshots when `horizon:snapshot` runs. Schedule it every five minutes in your console kernel; without it, the metrics repository has no data to display.

   ![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 articleIntercept: Middleware Guardrails for Laravel AI Agents](https://www.msaied.com/public/articles/intercept-middleware-guardrails-for-laravel-ai-agents) [Next articleMySQL Query Profiling in Laravel: EXPLAIN ANALYZE, Slow Query Log, and Index Tuning](https://www.msaied.com/public/articles/mysql-query-profiling-in-laravel-explain-analyze-slow-query-log-and-index-tuning)  

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

1. [Laravel Horizon in Production: Beyond the Pretty Dashboard](#laravel-horizon-in-production-beyond-the-pretty-dashboard)
2. [How Horizon Supervisors Actually Work](#how-horizon-supervisors-actually-work)
3. [Choosing the Right Balance Strategy](#choosing-the-right-balance-strategy)
4. [Reading Horizon Metrics Programmatically](#reading-horizon-metrics-programmatically)
5. [Graceful Termination and Long-Running Jobs](#graceful-termination-and-long-running-jobs)
6. [Memory Leaks and maxJobs](#memory-leaks-and-codemaxjobscode)
7. [Takeaways](#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/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  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)
