Skyline

Queue control

Weighted queues

Proportional queue priority that favours important work without starving the rest.

A Horizon supervisor with 'balance' => false serves its queues in strict left-to-right priority: the first queue is drained completely before the second is even looked at. That is exactly what you want until the first queue is never empty — at which point everything below it starves.

Turning balancing on solves starvation by giving each queue its own process pool, but you then pay for a floor of idle workers on the quiet queues. Weighted queues are the middle path: one shared pool, and a proportional policy where a heavier queue is checked more often but a lighter one is still always eventually served.

Configuring weights

Add a queueWeights map to the supervisor in config/horizon.php:

'environments' => [
    'production' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue' => ['high', 'default', 'low'],
            'balance' => false,
            'queueWeights' => [
                'high' => 3,
                'default' => 2,
                // 'low' is omitted, so it keeps the default weight of 1
            ],
        ],
    ],
],

No other change is needed. php artisan horizon picks the weights up and passes them to each worker it provisions.

What the weights actually mean

Weights are not a static reordering of the queue list. On every poll, the worker builds a fresh ordering by drawing queues without replacement, each with probability proportional to its weight. With 3 : 2 : 1 above, high is checked first about 50% of the time, default about 33%, and low about 17% — and whichever queue is drawn first, the others are still checked in that same poll if it turns out to be empty.

That distinction is what prevents starvation. A weight of 2 means "twice as likely to be looked at first", not "must be empty before anything else runs".

  • Queues you omit default to a weight of 1. You only need to list the queues whose weight differs from the default.
  • Weights are relative. [3, 2, 1] and [30, 20, 10] behave identically.
  • Zero and negative weights are ignored and fall back to the default of 1. There is no way to disable a queue with a weight — remove it from the queue list instead.
  • Paused queues are skipped during the scan, so weighting composes with per-queue pausing.

Only with balance => false

Weights only mean something when a single worker serves several queues. Under 'simple' or 'auto' balancing each queue already gets its own process pool, so there is no ordering left to weight. Combining the two is a configuration error rather than a silent no-op — provisioning throws:

The [supervisor-1.queueWeights] option only applies when [balance] is false;
each queue gets its own pool when balancing.

Blocking connections (block_for)

If your Redis queue connection sets block_for, a worker with nothing to do blocks on Redis rather than polling in a hot loop. Naively combining that with a per-poll weighted ordering would be a bug: the worker would block on whichever queue happened to be drawn first, and a job arriving on any other served queue would sit there until the block expired.

Skyline handles this. A worker first scans every served queue without blocking, in weighted order. Only when all of them are empty does it issue a single blocking read across all of the served queues at once, so it wakes the instant a job lands on any of them and then re-scans in weighted priority order. Priority is preserved and no job waits out a block_for window on an idle worker.

Redis Cluster

A single blocking read spanning several keys is not legal across hash slots on Redis Cluster. On a clustered connection Skyline falls back to polling each queue in priority order every 100 ms, bounded by block_for. The behaviour is the same; idle wake-up latency is marginally higher.

Choosing weights

Weights govern attention, not throughput. A queue with weight 3 does not get three times the workers — it gets looked at first three times as often. If high is empty most of the time, its weight costs nothing and the shared pool spends its time on default and low anyway.

A reasonable starting point is to weight by how much latency each queue can tolerate rather than by how much traffic it carries. A low-volume, latency-critical queue (password resets, webhooks) earns a high weight; a high-volume, latency-tolerant queue (report generation, backfills) earns a low one. Then watch the per-queue wait time on the metrics dashboard and adjust.

If a low-weight queue's wait time is still climbing without bound, weights are not your problem — you are short on workers, and no ordering policy will fix that.

Queue control, not just queue monitoring.

Skyline is a drop-in replacement for Laravel Horizon that lets you act on what you see — pause a queue, jump a job to the front, drain a backlog.

Sign up for early access