Priority-Aware Processing Lanes
Priority-aware processing lanes let nats-sinks prefer urgent work inside a
bounded batch without weakening commit-then-acknowledge processing. They are
designed for operators who already use message metadata such as priority,
classification, and labels to describe operational handling needs.
The feature is intentionally conservative. JetStream still decides which
messages are delivered to the pull consumer. nats-sinks only reorders the
messages that have already been fetched into the current batch. The sink then
receives one ordered batch, writes it durably, and the core runtime ACKs the
original JetStream messages only after sink success.
When To Use Priority Lanes
Use priority lanes when a sink instance may receive mixed-urgency events and you want high-priority messages to move earlier through the local sink write path during backlog drain. Examples include operational telemetry, urgent audit events, watch-floor messages, maintenance alarms, and mission-support events where some subjects or producers should receive faster local handling.
Do not use priority lanes as an authorization mechanism. Priority metadata can come from headers and can therefore be controlled by a publisher unless your NATS authorization, producer controls, and deployment policy say otherwise. Priority lanes are a scheduling hint after validation, not a trust boundary.
Processing Model
The scheduler runs after envelope normalization and optional payload
encryption, but before sink.write_batch(...).
sequenceDiagram
participant JS as JetStream
participant Runner as nats-sinks runner
participant Scheduler as Priority lane scheduler
participant Sink as Destination sink
participant Store as Durable destination
JS->>Runner: Fetch bounded batch
Runner->>Runner: Normalize NatsEnvelope metadata
Runner->>Runner: Apply optional payload encryption
Runner->>Scheduler: Order current batch by lanes
Scheduler-->>Runner: Ordered batch
Runner->>Sink: write_batch(ordered messages)
Sink->>Store: Write and commit
Store-->>Sink: Commit success
Sink-->>Runner: Success
Runner->>JS: ACK original messages
The central reliability rule does not change:
Receive -> validate -> order current batch -> write durably -> commit -> ACK
If lane validation fails, the message is treated as a permanent framework validation failure. If DLQ is enabled, the original message is ACKed only after DLQ publication succeeds. If DLQ is disabled, the message remains unacked and eligible for redelivery according to JetStream policy.
What Priority Lanes Do Not Guarantee
Priority lanes do not provide exactly-once processing. nats-sinks remains an
at-least-once delivery framework and sinks must remain idempotent.
Priority lanes do not provide strict total ordering across a stream, across subjects, across consumers, across processes, or across batches. They only affect the order of messages already returned by one pull fetch operation.
Priority lanes do not bypass sink durability. A lower-priority message in the same batch may still be ACKed after the batch succeeds, because the sink writes the ordered batch as one durable operation. Future implementations may add separate lane workers, but those workers must still preserve ACK-last behavior.
Configuration
Priority lanes live under delivery.priority_lanes. They are disabled by
default, so existing deployments keep sink delivery order exactly as returned
by JetStream fetch.
{
"delivery": {
"batch_size": 100,
"priority_lanes": {
"enabled": true,
"default_lane": "routine",
"unknown_priority_action": "default_lane",
"max_priority_value_length": 64,
"lanes": [
{
"name": "urgent",
"priorities": ["urgent", "immediate"],
"weight": 3
},
{
"name": "routine",
"priorities": ["normal", "routine"],
"weight": 1
}
]
}
}
}
Priority metadata itself is still configured through message_metadata. That
means you can set global defaults and subject-specific defaults without putting
subject names in the lane policy:
{
"message_metadata": {
"priority": {
"header": "Nats-Sinks-Priority",
"default": "routine"
},
"rules": [
{
"subject": "alerts.>",
"priority": "urgent"
},
{
"subject": "training.>",
"priority": "routine"
}
]
}
}
With this configuration, a message on alerts.sensor.contact that does not
carry a priority header receives the urgent default before lane scheduling.
A message that explicitly carries Nats-Sinks-Priority: routine remains
routine because publisher-supplied headers are authoritative after NATS
authorization has admitted the message.
Field Reference
| Field | Required | Default | Valid values | Description |
|---|---|---|---|---|
enabled |
no | false |
true or false |
Enables in-batch priority scheduling. |
default_lane |
no | default |
Configured lane name | Lane used for missing priorities and, by default, unknown priorities. |
unknown_priority_action |
no | default_lane |
default_lane or reject |
Controls whether unknown but syntactically safe priority values are downgraded to the default lane or rejected as permanent validation failures. |
max_priority_value_length |
no | 64 |
Integer 1 to 256 |
Maximum length for message priority metadata when lanes are enabled. Oversized values are rejected. |
lanes[].name |
yes | none | Lowercase letter followed by lowercase letters, digits, _, or -; max 64 characters |
Stable lane name. Lane names should be operationally boring and non-sensitive because they may appear in configuration and documentation. |
lanes[].priorities |
no | [] |
String list | Priority values that map to the lane. Matching is case-insensitive. A value may appear in only one lane. |
lanes[].weight |
no | 1 |
Integer 1 to 100 |
Weighted round-robin share for this lane inside a mixed batch. |
The default policy is equivalent to this:
{
"delivery": {
"priority_lanes": {
"enabled": false,
"default_lane": "default",
"unknown_priority_action": "default_lane",
"max_priority_value_length": 64,
"lanes": [
{
"name": "default",
"priorities": [],
"weight": 1
}
]
}
}
}
Weighted Round-Robin Behavior
Weights control how many messages the scheduler takes from a lane before checking the next lane. The scheduler repeats that pass until every lane bucket is empty.
For example, with urgent.weight = 2 and routine.weight = 1, this fetched
batch:
| Fetched order | Priority | Lane |
|---|---|---|
| 1 | urgent |
urgent |
| 2 | urgent |
urgent |
| 3 | urgent |
urgent |
| 4 | routine |
routine |
| 5 | routine |
routine |
is delivered to the sink as:
| Sink order | Original stream sequence | Priority |
|---|---|---|
| 1 | 1 | urgent |
| 2 | 2 | urgent |
| 3 | 4 | routine |
| 4 | 3 | urgent |
| 5 | 5 | routine |
This prevents routine work from being completely starved inside the current batch while still preferring urgent work.
Security Notes
Treat priority as untrusted input unless your deployment controls every publisher. The scheduler validates priority values when lanes are enabled:
- missing priority becomes the default lane;
- unknown priority becomes the default lane unless
unknown_priority_actionisreject; - control characters are rejected;
- oversized values are rejected;
- duplicate priority mappings across lanes are rejected at startup.
Use subject-specific defaults when priority should be assigned by operator policy rather than by publishers. Use NATS account permissions to decide who is allowed to publish to the subject families that receive urgent defaults.
Metrics
Priority lane metrics are aggregate by default. They do not include subjects, message IDs, priority strings, classification values, labels, payload fields, or stream sequence values.
| Metric suffix | Type | Meaning |
|---|---|---|
priority_lane_batches_total |
counter | Batches ordered by enabled priority-lane scheduling. |
priority_lane_messages_total |
counter | Messages evaluated by priority-lane scheduling. |
priority_lane_defaulted_total |
counter | Messages sent to the default lane because priority was missing or unknown. |
priority_lane_rejected_total |
counter | Messages rejected because priority metadata violated policy. |
current_priority_lanes_active |
gauge | Number of configured lanes represented in the active scheduled batch. |
Example:
nats-sink-metrics show .local/nats-sinks/metrics.json --metric "priority_lane_*"
Example output:
kind name value
counter priority_lane_batches_total 12
counter priority_lane_messages_total 768
counter priority_lane_defaulted_total 41
counter priority_lane_rejected_total 0
The active-lane gauge can be queried separately:
nats-sink-metrics get .local/nats-sinks/metrics.json current_priority_lanes_active --default 0
Operational Guidance
Start with small weights. A policy such as urgent = 3, routine = 1 is often
easier to reason about than very large ratios. Large weights can create the
appearance of starvation in small batches even though the scheduler eventually
checks every active lane.
Keep delivery.batch_size bounded. Priority lanes order the current batch in
memory, so the existing batch-size limit is also the scheduling memory limit.
Keep idempotency enabled at the sink. Priority lanes can change sink delivery order inside a batch, and redelivery can still happen after a crash between destination commit and JetStream ACK.
Review DLQ policy before setting unknown_priority_action to reject. Reject
mode is useful when priority values are part of a strict operational contract,
but malformed messages need a safe DLQ path so they do not block a stream
silently.