Dispatch Policies

The Strict and Weighted dispatch policies a Worker Group uses to choose which Queue to claim from next, with their public API, determinism, and work-conservation guarantee.


A Dispatch Policy is a Worker Group's rule for choosing which Queue 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.

DispatchPolicy.cs
public abstract record DispatchPolicy
{
    private DispatchPolicy() { }
 
    public abstract IReadOnlyList<string> Queues { get; }
}
MemberTypeMeaning
QueuesIReadOnlyList<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.

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.

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");
PropertyBehavior
OrderingFixed. The first Queue is highest priority; each later Queue is a fallback for when all earlier ones are empty.
Work-conservingYes. When higher Queues are empty, work flows to lower ones so no Worker idles while any served Queue has due work.
StarvationPermitted by design. A saturated high-priority Queue can strand lower Queues indefinitely.
Round-tripsOne 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.

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.

PropertyBehavior
Weight typeint, one per served Queue. Higher weight serves more often.
OrderingSmooth weighted round-robin. Claim opportunity is spread across passes in proportion to the weights.
Weights are relativeOnly the ratio matters for distribution. 6:3:1 and 60:30:10 spread work the same way.
Work-conservingYes. An empty Queue yields its share to the others, and unfilled slots reflow within the same pass so no Worker idles.
Round-tripsOne 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 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: the consumer side, pool capacity, PoolSize, and Backpressure.
  • Queues: named streams of jobs, Due Time ordering, and why priority is not a per-job property.
  • Limits & Defaults: the default Queue name, pool size, and other defaults in one place.

Found a problem on this page? Report an issue