# Weighted queues

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

Source: https://boring-observability.dev/skyline/docs/weighted-queues
Section: Queue control — Skyline for Laravel documentation
Updated: 2026-07-13

---

**Weighted queues** let a single pool of workers serve several Laravel queues in **proportional** rather than strict priority order. A `queueWeights` map on the supervisor makes a heavier queue more likely to be checked first, while guaranteeing that a lighter one is still always eventually served — so an important queue can be favoured without any queue starving.

It exists because the two options Horizon gives you are both wrong for this. A 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 the starvation by giving each queue its own process pool, but you then pay for a floor of idle workers on the quiet queues. Weights are the middle path: one shared pool, ordered by preference rather than by rank.

## Configuring weights

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

```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](https://boring-observability.dev/skyline/docs/pausing-queues).

## 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:

```text
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](https://boring-observability.dev/skyline/docs/metrics-and-trends) 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.


## Common questions

### What are weighted queues in Skyline?

A queueWeights map on a supervisor turns strict left-to-right queue priority into a proportional policy. On every poll the worker builds a fresh queue ordering by drawing queues without replacement, each with probability proportional to its weight — so a queue weighted 3 is checked first roughly three times as often as one weighted 1, while the lighter queue is still always eventually served.

### How do weighted queues prevent queue starvation?

Weights govern the order queues are checked in, not whether they are checked at all. A weight of 2 means "twice as likely to be looked at first", not "must be empty before anything else runs" — and whichever queue is drawn first, the others are still checked in the same poll if it turns out to be empty.

### Can I combine queueWeights with a balancing strategy?

No. Weights only mean something when one worker serves several queues, and under simple or auto balancing each queue already gets its own process pool, so there is no ordering left to weight. Combining them is a configuration error rather than a silent no-op: provisioning throws.
