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 thequeuelist 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.
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.