Laravel Horizon Supervisor Tuning &amp; Queue Metrics | 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 Reliable Job Throughput

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

 Go beyond the default Horizon setup. Learn how to tune supervisors per queue priority, use Horizon's metrics API for alerting, and design jobs that survive worker restarts without silent data loss.

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

ShareCopy linkCopied

 ![Laravel Horizon: Queue Metrics, Supervisor Tuning, and Reliable Job Throughput](https://cdn.msaied.com/313/b64554c2fce0b1ec694c929b7bff1e38.png) 

  On this page +1. [Laravel Horizon: Beyond the Default Config](#laravel-horizon-beyond-the-default-config)
2. [Supervisor Configuration That Actually Reflects Priority](#supervisor-configuration-that-actually-reflects-priority)
3. [Reading Horizon Metrics Programmatically](#reading-horizon-metrics-programmatically)
4. [Jobs That Survive Worker Restarts](#jobs-that-survive-worker-restarts)
5. [The Pattern: Idempotency Key + Atomic Check](#the-pattern-idempotency-key-atomic-check)
6. [Takeaways](#takeaways)

 Laravel Horizon: Beyond the Default Config
------------------------------------------

Most teams install Horizon, paste the default `config/horizon.php`, and call it done. That works until traffic spikes, a slow third-party API blocks your `default` queue, and your `critical` payment jobs sit waiting behind a backlog of email sends.

This article covers three concrete improvements: priority-aware supervisor configuration, reading Horizon's metrics API to drive alerting, and writing jobs that survive unexpected worker termination.

---

Supervisor Configuration That Actually Reflects Priority
--------------------------------------------------------

Horizon's `supervisors` key maps directly to separate worker pools. The key insight is that each supervisor is an independent process group — they do not share a queue slot budget.

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

```

`balance => auto` lets Horizon scale workers proportionally to queue depth. Use `simple` for critical queues where you want a fixed floor of workers always ready, not waiting for the autoscaler to react.

`balanceCooldown` (seconds) prevents thrashing — Horizon won't add or remove workers more often than this interval. Set it higher (5–10) on queues with bursty but short-lived spikes.

---

Reading Horizon Metrics Programmatically
----------------------------------------

Horizon stores throughput and runtime metrics in Redis. You can read them via the `Metrics` repository to drive custom dashboards or Slack alerts.

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

$metrics = app(MetricsRepository::class);

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

if ($throughput < 10 && $runtime > 5000) {
    // Alert: critical queue is slow and under-processed
    Log::channel('slack')->critical('Horizon critical queue degraded', [
        'throughput' => $throughput,
        'avg_runtime_ms' => $runtime,
    ]);
}

```

Wrap this in a scheduled command running every minute:

```php
// routes/console.php
Schedule::command('horizon:metrics:check')->everyMinute()->withoutOverlapping();

```

This gives you alerting without a paid APM tool for queue-specific health.

---

Jobs That Survive Worker Restarts
---------------------------------

When you run `horizon:terminate` or deploy a new release, Horizon sends `SIGTERM` to workers. A job mid-execution gets re-queued if it hasn't been deleted — but only if you haven't already committed a side effect.

### The Pattern: Idempotency Key + Atomic Check

```php
class ChargeSubscription implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public string $idempotencyKey;

    public function __construct(public int $subscriptionId)
    {
        $this->idempotencyKey = 'charge:' . $subscriptionId . ':' . now()->format('Y-m-d');
    }

    public function handle(PaymentGateway $gateway): void
    {
        $alreadyCharged = Cache::add(
            $this->idempotencyKey,
            true,
            now()->addDay()
        );

        if (! $alreadyCharged) {
            return; // Re-queued after SIGTERM — skip silently
        }

        $gateway->charge($this->subscriptionId);
    }
}

```

`Cache::add` is atomic on Redis — it only sets the key if it does not exist, returning `false` if the key was already present. This prevents double-charges when a job is re-queued after an interrupted worker.

---

Takeaways
---------

- **Separate supervisors per priority** — never let low-priority jobs compete for the same worker pool as critical ones.
- **Use `balance => simple` with a `minProcesses` floor** for queues where latency matters more than cost.
- **`MetricsRepository` is public API** — use it for custom alerting before reaching for an APM.
- **`Cache::add` is your idempotency primitive** — combine it with a deterministic key to make re-queued jobs safe.
- **`balanceCooldown` prevents autoscaler thrashing** — tune it per queue's burst profile, not globally.

- [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)
- [performance](https://www.msaied.com/public/articles?search=performance)

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

  How does Horizon's `balance =&gt; auto` differ from `balance =&gt; simple`?`auto` scales worker count proportionally to queue depth across all queues in that supervisor, redistributing processes dynamically. `simple` keeps a fixed number of workers per queue without redistribution. Use `simple` when you need a guaranteed minimum for latency-sensitive queues.

   Will a job be lost if a Horizon worker receives SIGTERM mid-execution?No — Horizon re-queues the job if it has not been deleted from Redis yet. The risk is duplicate execution, not data loss. Guard against duplicates with an idempotency key stored atomically in Redis via `Cache::add`.

   Can I use `MetricsRepository` outside of Horizon's dashboard?Yes. It is bound in the service container and can be resolved anywhere in your application. Horizon must be running and processing jobs for the metrics to be populated, but reading them is safe from any context including scheduled commands.

   ![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 articleExtending Laravel Collections: Macros, Mixins, and Higher-Order Proxies](https://www.msaied.com/public/articles/extending-laravel-collections-macros-mixins-and-higher-order-proxies) [Next articleLivewire v3 Islands, Lazy Components, and Deferred Loading in Practice](https://www.msaied.com/public/articles/livewire-v3-islands-lazy-components-and-deferred-loading-in-practice-1)  

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

1. [Laravel Horizon: Beyond the Default Config](#laravel-horizon-beyond-the-default-config)
2. [Supervisor Configuration That Actually Reflects Priority](#supervisor-configuration-that-actually-reflects-priority)
3. [Reading Horizon Metrics Programmatically](#reading-horizon-metrics-programmatically)
4. [Jobs That Survive Worker Restarts](#jobs-that-survive-worker-restarts)
5. [The Pattern: Idempotency Key + Atomic Check](#the-pattern-idempotency-key-atomic-check)
6. [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)
