# Queues

A Queue is a named stream of jobs. Every job belongs to exactly one Queue, declared on its
job type and overridable per enqueue. Within a Queue, jobs are claimed strictly in Due Time
order, oldest-due first, so a Queue behaves like a time-ordered stream of due work rather
than a priority heap. There are no per-job priority numbers. Priority is expressed on the
consumer side by a Worker Group's Dispatch Policy, which decides which served Queue to claim
from next. Each Queue can carry one optional Concurrency Limit that caps how many of its jobs
run at once across the whole deployment, and a Queue can be paused and resumed cluster-wide
by an operator.

## Assigning a job to a Queue

A job's Queue is set with the `Queue` property on its `[Job]` attribute. When you omit it,
the job goes to the Queue named `"default"`.

```csharp title="OrderCharged.cs"
[Job("order-charged", Queue = "billing")]
public sealed record OrderCharged(Guid OrderId);
```

The Queue on the attribute is the default for that job type. You can override it at a specific
call site with the optional `queue` parameter on `EnqueueAsync` and `EnqueueDependencyAsync`.
Passing `null`, the default, uses the job type's registered Queue.

```csharp title="ChargeOrder.cs"
// Goes to "billing", the job type's registered Queue.
await client.EnqueueAsync(new OrderCharged(orderId), DateTimeOffset.UtcNow);

// Same job type, routed to a different Queue for this one call.
await client.EnqueueAsync(new OrderCharged(orderId), DateTimeOffset.UtcNow, queue: "billing-urgent");
```

The [Jobs and Handlers](/docs/core-concepts/jobs-and-handlers) page covers the `[Job]`
attribute in full. The pattern is to declare a sensible default Queue on the type and reach
for the enqueue-time override only when a particular call needs a different one.

## How a Queue is claimed

Within a Queue, jobs are claimed in Due Time order. The oldest-due job is taken first, and
ties are broken by enqueue order, so two jobs with the same Due Time are claimed in the order
they were enqueued. A claim never returns a job whose Due Time is still in the future. Only
currently-due jobs are claimable, which means a plain enqueue and a future-scheduled job ride
the same ordering. An enqueue for "now" is simply a job whose Due Time is now.

A paused Queue yields nothing on claim until it is resumed. The same is true of a Queue that
has hit its Concurrency Limit. In both cases a Worker looking for work passes over that Queue
and moves on to the next one it serves.

## No per-job priority

BackWave has no per-job priority number. There is no priority integer on a job and no way to
make one job jump ahead of another within the same Queue. Order inside a Queue is Due Time,
full stop. This is a deliberate choice, not a missing feature.

Per-job priority numbers cause problems that get worse over time. They make starvation
invisible: a steady stream of high-priority jobs can quietly strand lower ones with nothing
in the system reporting that it is happening. They also push an ordering cost into every
claim query for the life of the deployment, and they create a second prioritization mechanism
alongside the one Worker Groups already give you, so the same intent can be expressed two
ways. Priority boosting, where a waiting job's priority climbs as it ages, was set aside for a
related reason: it makes the simple question "when will this job run?" impossible to answer.

To prioritize work in BackWave, use separate named Queues and let a Worker Group's Dispatch
Policy decide how to draw from them. Two policies are available:

- **Strict** serves Queues in a fixed priority order. An earlier Queue is always tried before
  a later one, and a later Queue is reached only when every earlier one has no due work. A
  continuously busy high-priority Queue can starve the lower ones, which is the explicit,
  accepted trade-off of this policy.
- **Weighted** shares claim opportunity across Queues in proportion to integer weights, for
  example 6:3:1. Every Queue keeps making progress, and the distribution is deterministic
  with no random clumping.

Both policies are work-conserving: a Worker never sits idle while any Queue it serves has due
work. The [Worker Groups & Dispatch](/docs/core-concepts/worker-groups) page owns the full
mechanics of Strict and Weighted, including how weights are set and how a group registers the
Queues it serves.

## Concurrency Limits

A Queue can carry one optional Concurrency Limit. It caps how many of that Queue's jobs are
leased and executing at the same time across the entire deployment, not per node. The limit
is enforced at claim time: a Queue sitting at its limit yields nothing until a slot frees.

| Property | Behavior |
|---|---|
| Scope | Per-Queue, cluster-wide. One shared count across the whole deployment for that Queue. |
| What it bounds | How many of the Queue's jobs are leased and executing at once. |
| Enforced | At claim time. A Queue at its limit is skipped on claim. |
| Default | None. No cap is configured, so the Queue is uncapped. |
| Slot accounting | A slot in use is a currently-leased job. No separate counter is kept. |
| Slot release | A slot frees the moment a job reaches a terminal state or its Lease expires, so a crashed Worker never leaks one. |

You configure the limit on the job store your application registers, by calling
`SetConcurrencyLimitAsync` with the Queue name and the cap. Passing `null` clears the cap and
returns the Queue to uncapped.

```csharp title="ConfigureQueue.cs"
// Cap the "billing" Queue at 5 concurrent jobs across the whole cluster.
await jobStore.SetConcurrencyLimitAsync("billing", 5);

// Remove the cap.
await jobStore.SetConcurrencyLimitAsync("billing", null);
```

### Concurrency Limit versus per-node pool capacity

A Queue's Concurrency Limit and a node's pool capacity are two independent gates, and they do
not substitute for each other. The Concurrency Limit is one shared counter across the cluster
for a single Queue. A Worker Group's pool capacity is each node's own ceiling, set by
`PoolSize` and defaulting to 20, which bounds how many jobs that group runs concurrently on
that node. A node can be full on its own pool while the Queue's Concurrency Limit still has
free slots, and the Limit can be saturated while a node's pool is idle. The node side, pool
capacity and the Backpressure that pauses polling when a pool is full, is covered on the
[Worker Groups & Dispatch](/docs/core-concepts/worker-groups) page.

## Pausing a Queue

A Queue can be paused and resumed across the whole cluster by an operator. While a Queue is
paused, no Worker claims work from it, and jobs already running are unaffected and finish
normally. Resuming the Queue makes its due jobs claimable again. The
[The Dashboard](/docs/dashboard-operations/overview#what-operators-can-do) covers pausing and
resuming from the operations side.

## Where to go next

- [Worker Groups & Dispatch](/docs/core-concepts/worker-groups): the consumer side, Strict
  and Weighted Dispatch Policies, pool capacity, and Backpressure.
- [Scheduling](/docs/core-concepts/scheduling): Due Time, future-dated work, and recurring
  schedules that feed Queues.
- [Execution Model](/docs/core-concepts/execution-model): how claimed work is leased and run.
- [Jobs and Handlers](/docs/core-concepts/jobs-and-handlers): the `[Job]` attribute and where
  the `Queue` property is declared.
- [Dispatch Policies](/docs/reference/dispatch-policies): the reference for Strict and
  Weighted.
- [Limits & Defaults](/docs/reference/limits-and-defaults): the default Queue name, pool size,
  and other defaults in one place.
