Laravel Horizon: Queue Tuning &amp; Job Reliability | 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 Deep Dive: Queue Tuning, Supervisor Strategies, and Job Reliability

 Laravel Horizon Deep Dive: Queue Tuning, Supervisor Strategies, and Job Reliability
====================================================================================

 Move beyond the default Horizon config. Learn how to tune supervisor processes, balance queue priorities, handle long-running jobs safely, and instrument your queues for production reliability.

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

ShareCopy linkCopied

 ![Laravel Horizon Deep Dive: Queue Tuning, Supervisor Strategies, and Job Reliability](https://cdn.msaied.com/186/198e117fc9baa8699ff73709f64a3c79.png) 

  On this page +1. [Why the Default Horizon Config Will Bite You in Production](#why-the-default-horizon-config-will-bite-you-in-production)
2. [Supervisor Strategies: Separate Concerns by Queue](#supervisor-strategies-separate-concerns-by-queue)
3. [Long-Running Jobs: Timeout, Memory, and the Daemon Loop](#long-running-jobs-timeout-memory-and-the-daemon-loop)
4. [Memory Leak Mitigation](#memory-leak-mitigation)
5. [Instrumenting Queues: Events and Metrics](#instrumenting-queues-events-and-metrics)
6. [Takeaways](#takeaways)

 Why the Default Horizon Config Will Bite You in Production
----------------------------------------------------------

The `horizon.php` stub ships with a single `default` supervisor, `auto` balance mode, and `maxProcesses: 1`. That is fine for a demo. In production it means one slow job can stall your entire queue, memory leaks accumulate silently, and you have no visibility into which queue is the bottleneck.

This article walks through the decisions that actually matter.

---

Supervisor Strategies: Separate Concerns by Queue
-------------------------------------------------

Horizon's supervisor config is the most important lever you have. Group queues by their latency contract, not by convenience.

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

```

**`simple` vs `auto` balance:** `simple` distributes processes evenly across queues in the list. `auto` watches queue depth and shifts workers toward the busiest queue. Use `auto` when queue depths are unpredictable; use `simple` when you need deterministic allocation.

`balanceMaxShift` and `balanceCooldown` prevent thrashing — Horizon will move at most 5 workers per 3-second cycle.

---

Long-Running Jobs: Timeout, Memory, and the Daemon Loop
-------------------------------------------------------

The `timeout` value in Horizon config maps directly to the `--timeout` flag passed to `queue:work`. When a job exceeds this, the worker process is killed via SIGKILL — not a graceful exception. Your job must be idempotent.

```php
class GenerateExportJob implements ShouldQueue
{
    public int $timeout = 540;   // job-level override
    public int $tries   = 2;
    public bool $failOnTimeout = true;

    public function handle(): void
    {
        // Checkpoint progress so a retry can resume
        Cache::put("export:{$this->exportId}:cursor", $this->cursor);

        // ... heavy work ...
    }

    public function retryUntil(): \DateTime
    {
        return now()->addHours(2);
    }
}

```

Set `$failOnTimeout = true` (Laravel 10+) so the job is marked failed rather than silently retried forever.

---

Memory Leak Mitigation
----------------------

Horizon workers are long-lived PHP processes. Every static cache, event listener registered inside a job, or Eloquent model that holds a reference will accumulate.

```php
// In a job that processes many models
public function handle(): void
{
    User::query()
        ->lazyById(200)
        ->each(function (User $user) {
            $this->process($user);
            // Detach the model from the identity map
        });

    // Force GC after heavy work
    gc_collect_cycles();
}

```

Set `memory` in the supervisor config to a value your server can sustain multiplied by `maxProcesses`. Horizon will restart a worker that exceeds its memory limit after the current job finishes.

---

Instrumenting Queues: Events and Metrics
----------------------------------------

Horizon emits first-class events. Hook into them for custom alerting without polling the dashboard.

```php
// app/Providers/AppServiceProvider.php
use Laravel\Horizon\Events\JobFailed;
use Laravel\Horizon\Events\LongWaitDetected;

public function boot(): void
{
    Event::listen(LongWaitDetected::class, function (LongWaitDetected $event) {
        Log::warning('Long queue wait', [
            'connection' => $event->connection,
            'queue'      => $event->queue,
            'seconds'    => $event->seconds,
        ]);
        // Push to your alerting system
    });

    Event::listen(JobFailed::class, function (JobFailed $event) {
        Metrics::increment('queue.job_failed', [
            'job' => class_basename($event->payload['displayName']),
        ]);
    });
}

```

`LongWaitDetected` fires when a queue's wait time exceeds the threshold set in `horizon.waits`. Configure it per queue:

```php
'waits' => [
    'redis:critical' => 5,   // alert after 5 seconds
    'redis:default'  => 60,
],

```

---

Takeaways
---------

- **Separate supervisors by latency contract** — never mix sub-second and multi-minute jobs in the same supervisor.
- **`auto` balance is not free** — tune `balanceMaxShift` and `balanceCooldown` to prevent worker thrashing.
- **Set `$failOnTimeout = true`** on long-running jobs so failures surface in the failed jobs table.
- **Memory limits are per-worker**, not per-supervisor — size them against your actual job footprint.
- **Hook `LongWaitDetected`** for proactive alerting instead of discovering queue backlogs after user complaints.
- **`retryUntil` beats `$tries`** for time-sensitive jobs where absolute deadline matters more than attempt count.

- [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 
---------------------------

  What is the difference between 'simple' and 'auto' balance modes in Horizon?'simple' divides processes evenly across all queues in the supervisor list. 'auto' monitors queue depth in real time and shifts workers toward whichever queue has the most pending jobs, subject to balanceMaxShift and balanceCooldown limits.

   How does Horizon handle a job that exceeds its timeout?Horizon sends SIGKILL to the worker process — there is no exception thrown inside the job. Set `$failOnTimeout = true` (Laravel 10+) so the job is recorded as failed. Always design long-running jobs to be idempotent so a retry can safely resume.

   Can I override the timeout at the job level rather than in the Horizon config?Yes. Defining a public `$timeout` property on the job class overrides the supervisor-level timeout for that specific job. The lower of the two values wins, so ensure your supervisor timeout is at least as high as your longest job-level timeout.

   ![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 articleLaravel Macros, Mixins, and Custom Collection Methods That Actually Ship](https://www.msaied.com/public/articles/laravel-macros-mixins-and-custom-collection-methods-that-actually-ship) [Next articleEvent Sourcing in Laravel: Aggregates, Projectors, and Reactors Without the Framework Tax](https://www.msaied.com/public/articles/event-sourcing-in-laravel-aggregates-projectors-and-reactors-without-the-framework-tax)  

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

1. [Why the Default Horizon Config Will Bite You in Production](#why-the-default-horizon-config-will-bite-you-in-production)
2. [Supervisor Strategies: Separate Concerns by Queue](#supervisor-strategies-separate-concerns-by-queue)
3. [Long-Running Jobs: Timeout, Memory, and the Daemon Loop](#long-running-jobs-timeout-memory-and-the-daemon-loop)
4. [Memory Leak Mitigation](#memory-leak-mitigation)
5. [Instrumenting Queues: Events and Metrics](#instrumenting-queues-events-and-metrics)
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)
