Priority groups
The worker pool shared work evenly. Every
worker on the shipping consumer pulled, and the server delivered messages
to whichever worker asked.
Some workloads need a different split. You might want one client to handle all the work until it fails, or a far-away client to stay idle unless the near ones fall behind. Even work sharing can't do either of those.
Priority groups let a pull consumer ask for those behaviors. They're designed in ADR-42. This page covers all three policies the server offers.
What a priority group is
A priority group is a name on a pull consumer, plus a policy that decides how the server hands out messages for that name.
You set two fields when you create the consumer:
PriorityGroups: the list of group names the consumer supports. Today a consumer uses exactly one group; naming more than one is accepted but only the first takes effect (see Pitfalls).PriorityPolicy: the rule the server applies, one ofoverflow,pinned_client, orprioritized.
Two of the three policies, overflow and pinned_client, need the consumer to
acknowledge its messages, so those examples use --ack explicit. They decide
what to do from counts the server keeps per client: overflow looks at how many
messages are waiting and unacknowledged, pinned_client at which client is
still pulling, and the server only keeps those counts when the consumer acks.
Prioritized just sorts pulls by a number and needs no acks.
Once a consumer has a policy, every pull must name its group. A pull that
leaves the group out is rejected with Bad Request - Priority Group missing.
The group on the pull and the group on the consumer must match.
So a priority group is a name on the consumer, a policy that governs it, and pulls that join the group by name. The rest of this page covers the two policies and the problem each one solves.
The overflow policy
Two regions can both process orders. us-east is close to the stream and
cheap to serve. us-west works too, but every message it pulls crosses
the country, costs more, and arrives slower. You want us-west to stay
idle unless us-east falls behind.
The overflow policy does this. Workers in us-east pull with no
threshold, so they always get messages. Workers in us-west pull with a
min_pending threshold: the server answers their pull only when the
consumer has at least that many messages waiting. Below the threshold
their pull gets nothing, the same as if the stream were empty.
Create an overflow consumer on the ORDERS stream:
nats consumer add ORDERS dispatch --overflow-groups regions --ack explicit --pull --defaults
The --overflow-groups regions flag sets the policy to overflow and
names the single group regions. Confirm it:
nats consumer info ORDERS dispatch
The configuration now carries the two priority fields:
Configuration:
Pull Mode: true
Ack Policy: Explicit
Priority Policy: overflow
Priority Groups: [regions]
The threshold goes on the pull request, not on the consumer. A
near-region worker pulls with no threshold. A far-region worker adds
min_pending: deliver only when the consumer has backed up past that many
waiting messages. min_ack_pending is a related threshold, counted
against unacknowledged messages instead; meeting either one triggers
delivery.
The nats consumer next command issues a plain pull and has no flag for
these thresholds, so the overflow pull below comes from a client library:
- CLI
#!/bin/bash
# Overflow policy: a standby region pulls only when the consumer has
# backed up past a min_pending threshold.
# Create an overflow pull consumer on ORDERS. --overflow-groups sets the
# policy to overflow and names the single group "regions". Overflow
# requires explicit acks.
nats consumer add ORDERS dispatch \
--overflow-groups regions \
--pull \
--ack explicit \
--defaults
# Inspect it — the configuration now shows Priority Policy: overflow and
# Priority Groups: [regions].
nats consumer info ORDERS dispatch
# The min_pending threshold lives on the pull request, which natscli's
# `nats consumer next` does not expose a flag for. A near-region worker
# pulls plainly and always gets messages:
nats consumer next ORDERS dispatch --count 5
The near-region worker, pulling without a threshold, empties the backlog
as fast as it processes. The far-region worker gets messages only when the
backlog crosses its min_pending threshold, takes the overflow, and goes
idle again once the near worker catches up.
The pinned_client policy
The overflow policy spreads work under load. The pinned_client policy sends all work to one client and keeps a standby ready to take over.
Consider an order pipeline that must process messages strictly in arrival order. Two clients run so that one can take over if the other fails, but only one may work at a time, or the ordering breaks. You want one active client and one standby.
The server picks one waiting pull and pins it. That client becomes the one that receives messages. Every other client's pull waits as a standby. If the pinned client stops pulling, because it crashed or went quiet longer than the pin timeout allows, the server pins a standby instead.
Create a pinned consumer:
nats consumer add ORDERS sequencer --pinned-groups ordered --pinned-ttl 90s --ack explicit --pull --defaults
Two flags do the work. --pinned-groups ordered sets the policy to
pinned_client and names the group ordered. --pinned-ttl 90s sets
how long the server waits for a pull from the pinned client before it
gives up and pins someone else.
The pin timeout must sit comfortably above the pull's expires value. The
pinned client needs time to pull, get its batch or time out, process, then
pull again, all before the timeout fires and costs it the pin. The
server's default timeout is two minutes; keep expires under a minute and
the whole cycle fits.
The pinned client earns and keeps the pin like this:
- CLI
#!/bin/bash
# Pinned_client policy: one active client gets all the work, with a
# standby ready to take over.
# Create a pinned consumer. --pinned-groups sets the policy to
# pinned_client and names the group "ordered". --pinned-ttl is how long
# the server waits for a pull from the pinned client before pinning a
# standby instead. Pinned consumers require explicit acks.
nats consumer add ORDERS sequencer \
--pinned-groups ordered \
--pinned-ttl 90s \
--pull \
--ack explicit \
--defaults
# Inspect it — the configuration shows Priority Policy: pinned_client,
# Priority Groups: [ordered], and Pinned TTL: 1m30s. The State block
# shows which client (if any) currently holds the pin.
nats consumer info ORDERS sequencer
# The Nats-Pin-Id handshake happens on the pull request, which natscli's
# `nats consumer next` does not drive. Pull a message to see delivery:
nats consumer next ORDERS sequencer --count 1
# Force the server to pick a new pinned client:
nats consumer unpin ORDERS sequencer ordered
The client and server agree on the pin through a header. When the server
pins a client, the first message it delivers carries a Nats-Pin-Id
header. The client reads that ID and sends it back on every later pull.
The server keeps serving the client that presents the matching ID and
parks the others.
The client gives up the pin in one of two ways. If it falls silent past
the timeout, the server pins a standby, and the old client's next pull,
still carrying the now-stale ID, comes back with a 423 status. The
client clears its stored ID and pulls without it, joining the standby
pool again. The 423 rules and the pinned and unpinned advisories are in
the Consumer API reference.
The other way is an operator forcing a switch. nats consumer unpin
clears the current pin and makes the server choose again:
nats consumer unpin ORDERS sequencer ordered
The command takes the stream, the consumer, and the group name. It reports the client it dropped:
Unpinned client <client-id> from Priority Group ORDERS > sequencer > ordered
To check who's pinned without forcing a change, read the consumer's
state. nats consumer info ORDERS sequencer shows the live pin in its
State block:
State:
Priority Groups: ordered: pinned <client-id> at 2026-06-02T10:14:22Z
A group with no active client reads No client. To list every fully
pinned consumer at once, run nats consumer find ORDERS --pinned.
:::note Client support varies
The pinned-client steps (storing Nats-Pin-Id, sending it back, handling
the 423) work in the Go and Java clients today. Other clients let you set
the configuration fields but may not yet run the client-side pinning loop.
Check your client's reference before relying on it.
:::
The prioritized policy
The overflow policy makes a standby wait for a backlog to build. Sometimes you want the opposite: hand work to the next region the instant the closer one stops asking, with no threshold and no delay.
us-east is close and cheap but resource-constrained. Whenever it has
capacity it should take the work; when it doesn't, us-west should pick up
right away, and eu-west only when neither US region is pulling. That's a
hierarchy, not a threshold.
The prioritized policy serves pulls in priority order. Each pull carries a
priority from 0 to 9, and the server hands messages to the lowest number
present first; pulls at the same priority share round-robin. So us-east
pulls at priority 0, us-west at 1, and eu-west at 2: work goes to
us-east whenever it's asking, falls to us-west the moment us-east isn't,
and reaches eu-west only when neither is pulling.
Create a prioritized consumer:
nats consumer add ORDERS dispatch --prioritized-groups regions --pull --defaults
--prioritized-groups regions sets the policy to prioritized and names the
single group regions. Unlike the other two policies, prioritized keeps no
per-client counts, so it doesn't require explicit acks.
The priority rides on the pull request, the same place overflow's thresholds
go, so nats consumer next can't set it and the pull comes from a client
library:
- CLI
#!/bin/bash
# Prioritized policy: each pull carries a 0-9 priority and the server serves
# the lowest number first, so nearer workers get first refusal and farther
# ones pick up the instant the nearer ones go quiet.
# Create a prioritized pull consumer on ORDERS. --prioritized-groups sets the
# policy to prioritized and names the single group "regions". Prioritized
# sorts pulls by number and tracks no per-client counts, so unlike overflow
# and pinned_client it needs no explicit acks.
nats consumer add ORDERS dispatch \
--prioritized-groups regions \
--pull \
--defaults
# Inspect it — the configuration shows Priority Policy: prioritized and
# Priority Groups: [regions].
nats consumer info ORDERS dispatch
# The priority rides on the pull request, which natscli's `nats consumer next`
# does not expose a flag for. A worker pulls plainly (priority 0) and is
# served ahead of any higher-numbered pull:
nats consumer next ORDERS dispatch --count 5
This is the immediate counterpart to overflow. Overflow waits for the backlog to cross a threshold before a standby gets anything, which avoids churn but makes far regions wait. Prioritized shifts work the moment a higher-priority puller goes quiet, with no delay, at the cost of some flip-flop as work moves between regions.
The full set of priority-group options, the 423 protocol, the
PriorityGroupState fields, and the advisories live in
Reference → Consumer API and in
ADR-42.
Pitfalls
Priority groups have a small surface, but a few details are easy to miss.
One group per consumer. A consumer uses exactly one priority group
today. The --overflow-groups and --pinned-groups flags take a comma
list, so passing two looks legal, and the server accepts it, but it uses
only the first group and ignores the rest. Multiple groups per consumer is
planned for a future server release. To split work by region or tier now,
run separate consumers on the same stream, each with its own group.
- CLI
#!/bin/bash
# A consumer acts on exactly one priority group. The --overflow-groups
# and --pinned-groups flags accept a comma list, so it is easy to pass
# two by accident.
# NOT THIS: two group names. The server accepts the create without error,
# but it uses only the first group (regions) and silently ignores the
# rest. Multiple groups per consumer is reserved for a future release.
nats consumer add ORDERS dispatch \
--overflow-groups regions,backup \
--pull \
--ack explicit \
--defaults
# DO THIS: name a single group. To split work by region or tier, run
# separate consumers, each with its own group, on the same stream.
nats consumer add ORDERS dispatch \
--overflow-groups regions \
--pull \
--ack explicit \
--defaults
The pin does not give one client sole ownership. The server can switch
the pinned client while that client still believes it holds the pin, so a
long-running handler can finish work the server already gave to someone
else. A pull that carries a now-stale Nats-Pin-Id comes back with a
423; clear the stored ID and pull without it to join the standby pool
again. If the same message must never be processed twice, use explicit
acks and handlers that are safe to run more than once, not the pin alone.
A quiet pinned client keeps the pin. The pin only resets when the
pinned client pulls again within --pinned-ttl. A client that holds the
pin but stops pulling, for example because it's stuck on a slow handler,
keeps every other client parked until the timeout fires. Keep each pull's
expires comfortably under the pin timeout so the client always pulls
again in time to renew. Which node in a cluster serves these pulls is
covered in clustering.
Where you are
A priority group is always one group plus a policy that decides how the server
hands out pulls: reach for overflow to spill work to a standby only under
load, pinned_client for one active worker with a standby ready to take over,
and prioritized for an immediate, lowest-first hierarchy across regions.
What's next
A consumer doesn't have to be running at all. The next page pauses a consumer, stopping it from delivering for a set window, and shows when that's the right tool.
See also
- Reference → Consumer API —
the priority-group config fields,
PriorityGroupState, the423protocol, and the pinned/unpinned advisories. - ADR-42 — the design of pull consumer priority groups.
- A pool of workers — the even work-sharing this page steers away from.