# Dispatch Policies

A Dispatch Policy is a [Worker Group's](/docs/core-concepts/worker-groups) rule for choosing
which [Queue](/docs/core-concepts/queues) to claim from next. Priority lives here, on the
consumer's policy, and never as a per-job property. There are exactly two policies: **Strict**,
which serves Queues in a fixed priority order and accepts starvation of the tail, and
**Weighted**, which shares claim opportunity across Queues in proportion to integer weights
using smooth weighted round-robin. Both policies are work-conserving: a Worker never idles
while any Queue it serves has due work. Both are fully deterministic, with no randomness in the
selection.

## The `DispatchPolicy` type

`DispatchPolicy` is a closed abstract record with two subtypes, `Strict` and `Weighted`. The
set is exhaustive: there is no third policy, and none can be defined outside the library. Every
policy exposes the ordered list of Queue names it serves.

```csharp title="DispatchPolicy.cs"
public abstract record DispatchPolicy
{
    private DispatchPolicy() { }

    public abstract IReadOnlyList<string> Queues { get; }
}
```

| Member | Type | Meaning |
|---|---|---|
| `Queues` | `IReadOnlyList<string>` | The Queues this policy serves, in declaration order. A Worker claims only from these Queues, and the policy decides which to try first on each pass. |

A Worker Group declares its policy through the required `Policy` property on its options. Every
Worker Group must set one; there is no default policy in production configuration.

```csharp title="RegisterWorkerGroup.cs"
builder.AddWorkerGroup(new WorkerGroupOptions
{
    Name = "notifications",
    Policy = new DispatchPolicy.Strict("emails", "reports"),
    // ... remaining group options
});
```

## Strict

`Strict` serves its Queues in strict priority order. An earlier Queue is always tried before a
later one, and a later Queue is reached only when every earlier Queue has no due work. A
continuously busy high-priority Queue can therefore starve the tail indefinitely. This is the
deliberate, accepted trade-off of strict priority, not a defect.

You construct it with the Queues to serve, highest priority first. A convenience constructor
takes a `params` array so you can list Queue names inline.

```csharp title="StrictPolicy.cs"
// "critical" is always preferred over "bulk". "bulk" is only
// reached when "critical" has no due work.
var policy = new DispatchPolicy.Strict("critical", "bulk");
```

| Property | Behavior |
|---|---|
| Ordering | Fixed. The first Queue is highest priority; each later Queue is a fallback for when all earlier ones are empty. |
| Work-conserving | Yes. When higher Queues are empty, work flows to lower ones so no Worker idles while any served Queue has due work. |
| Starvation | Permitted by design. A saturated high-priority Queue can strand lower Queues indefinitely. |
| Round-trips | One claim per pass over all served Queues in priority order. |

## Weighted

`Weighted` shares claim opportunity across its Queues in proportion to their weights using
smooth weighted round-robin. A higher-weight Queue is served more often, but every Queue keeps
making progress, and a Queue with no due work yields its turn to the others. Weights such as
6:3:1 are honored exactly and deterministically, with no random clumping.

You construct it with each served Queue paired with its relative integer weight. There is no
`params` convenience overload; pass value tuples.

```csharp title="WeightedPolicy.cs"
// "a" is served roughly six times as often as "c", "b" three
// times as often. Every Queue still makes progress.
var policy = new DispatchPolicy.Weighted([("a", 6), ("b", 3), ("c", 1)]);
```

The `Queues` list is derived from the weights in declaration order.

| Property | Behavior |
|---|---|
| Weight type | `int`, one per served Queue. Higher weight serves more often. |
| Ordering | Smooth weighted round-robin. Claim opportunity is spread across passes in proportion to the weights. |
| Weights are relative | Only the ratio matters for distribution. 6:3:1 and 60:30:10 spread work the same way. |
| Work-conserving | Yes. An empty Queue yields its share to the others, and unfilled slots reflow within the same pass so no Worker idles. |
| Round-trips | One claim per served Queue that has a positive allocation on the pass. |

### Weight validation

A `DispatchPolicy.Weighted` requires at least one Queue and every weight to be at least 1.
Registering a Weighted policy that has no Queues, or any weight of 0 or below, raises an
`ArgumentException` with the message `Weighted dispatch needs at least one queue, all weights
>= 1.` This is validated when the Worker Group starts running, not when the policy
value is constructed, so the error surfaces as the group comes up rather than at the call that
builds the record.

## Determinism

Both policies are fully deterministic. No random number generator takes part in the selection.
Strict is a fixed ordering. Weighted is smooth weighted round-robin, and ties break by
declaration order, so the lower-indexed Queue wins a tie.

Weighted spreads each Queue's turns evenly across a pass rather than serving one Queue's whole
share in a burst. A 6:3:1 weighting repeats the exact period-10 pattern
`a, b, a, a, b, a, c, a, b, a`. A 5:1:1 weighting repeats `a, a, b, a, c, a, a`. Over a long
run the served counts match the weights exactly: distributing 1000 claim opportunities across
6:3:1 yields 600, 300, and 100. The same schedule is produced on every run.

An idle Queue that later becomes active cannot bank an unbounded burst. Its accumulated claim
credit is bounded, so a Queue that sat empty and then has work catches up by a bounded amount
rather than dominating the pass.

## Work conservation

Work conservation is the guarantee that a Worker never idles while any Queue it serves has due
work. It holds for both policies, and it is what makes separate Queues a safe way to express
priority: idle capacity is always put to use on whatever due work exists.

Under Strict, when the priority Queue is empty the Worker claims from lower Queues instead of
sitting idle. Under Weighted, an empty Queue yields its share to the Queues that do have work.
When a Queue has fewer due jobs than its share of a pass, the unfilled slots reflow to the
other served Queues within that same pass, so the pass still fills the pool from due work
wherever it exists. Across a pass the split is approximate; in aggregate over many passes it
matches the configured weights.

Because Weighted advances its accounting only for claims that are actually issued, a pass that
is cut short does not run a Queue into a deficit. A Queue whose turn was planned but not reached
keeps its credit rather than being charged for work it never got to serve, so interruptions do
not starve a Queue or skew the long-run distribution.

## Claim sizing and Backpressure

Both policies claim only into free Worker capacity. A pass computes how many slots are free in
the pool and claims no more than that; when nothing is free it makes no claim at all. This is
how Backpressure works on the consumer side: a full pool stops pulling new work regardless of
which policy is in force. Strict fills the free capacity in a single priority-ordered claim.
Weighted sizes a per-Queue batch across the free capacity and issues one claim per Queue that
has a positive allocation, so filling the pool costs one claim per served Queue rather than one
per job. Pool capacity, `PoolSize`, and Backpressure are covered on the
[Worker Groups & Dispatch](/docs/core-concepts/worker-groups) page.

## Choosing a policy

Reach for Strict when one class of work must always be preferred over another and it is
acceptable for a busy high-priority Queue to strand the rest. Reach for Weighted when several
Queues should all keep making progress in a controlled ratio, with no Queue able to shut the
others out. Separate Worker Groups in one process can serve disjoint Queues under different
policies, so a Strict group and a Weighted group can run side by side against different Queues.

## Where to go next

- [Worker Groups & Dispatch](/docs/core-concepts/worker-groups): the consumer side, pool
  capacity, `PoolSize`, and Backpressure.
- [Queues](/docs/core-concepts/queues): named streams of jobs, Due Time ordering, and why
  priority is not a per-job property.
- [Limits & Defaults](/docs/reference/limits-and-defaults): the default Queue name, pool size,
  and other defaults in one place.
