NEW: Free hands-on NATS workshops. Live sessions on NATS fundamentals, leaf nodes, AI agents on NATS, & more.
All posts

A common community question is how to handle a JetStream workqueue stream when a task is published but no consumer exists to process it. Without a matching consumer, the stream can continue to retain that message, which may be surprising if the application expected unclaimed work to expire, be dropped, or be routed to a dead-letter queue.

Short answer

JetStream does not provide an automatic fallback consumer or automatic dead-letter queue for workqueue messages that have no matching consumer.

You generally have four choices:

  1. Use a retention policy that matches your desired semantics.
  2. Add stream limits such as max age, max messages, or max bytes as a safety bound.
  3. Implement an application-level fallback or cleanup process.
  4. Implement explicit dead-letter behavior in your consumers or maintenance tooling.

The right answer depends on whether a message must wait for a future worker, or whether it should only exist when there is current consumer interest.

WorkQueue retention versus Interest retention

JetStream retention policy is the first design choice to revisit.

WorkQueue retention

A stream using workqueue retention is appropriate when each message represents a unit of work that should be handled once, by a single consumer. A message is retained until it is acknowledged, after which the server removes it, subject to the stream’s configured limits.

Workqueue retention also enforces a structural constraint: each message may be consumed by only one consumer, so consumers on the stream must have non-overlapping filter subjects. The server rejects a new consumer whose filter subject overlaps an existing one. This constraint matters as soon as you start thinking about “fallback” consumers, as discussed below.

That single-consumer model also means a message can remain in the stream indefinitely if no consumer ever matches and acknowledges it. Workqueue retention is a good fit when a task must not be silently discarded simply because workers are temporarily offline or not yet created — but it is also exactly the behavior that produces an unbounded backlog of orphaned messages.

Interest retention

Interest-based retention keeps a message only while there is consumer interest in it. Interest is evaluated at publish time: when a message is published, the server records which consumers have a filter subject matching that message, and retains the message until all of those consumers have acknowledged it. As with workqueue retention, the message is removed once it has been fully acknowledged.

The key difference for the orphaned-message problem is what happens when there is no interest. If no consumer with a matching filter subject exists at the moment a message is published, an interest-retention stream does not store the message at all. Because interest is fixed at publish time, a consumer created later does not retroactively receive that message — so interest retention is not appropriate if a task must wait for a worker that does not exist yet.

This is useful when the desired behavior is closer to: deliver the message only to consumers that are already registered, and otherwise let it disappear.

However, interest retention still requires operational discipline. A durable consumer that was created and then forgotten continues to represent interest, causing messages to accumulate until that consumer is deleted, catches up, or stream limits remove the data. Changing the retention policy does not remove the need to manage the consumer lifecycle.

There is no automatic fallback consumer

JetStream consumers are explicit. The server does not automatically say, in effect, no one wanted this message, so deliver it to some other consumer.

If you need fallback behavior, model it directly. Common approaches include:

  • A dedicated consumer that covers subjects no other consumer handles. On a workqueue stream this only works for subjects that are not already claimed by another consumer, because consumer filter subjects on a workqueue stream cannot overlap. Such a consumer cannot act as an overlapping catch-all layered on top of existing consumers.
  • A scheduled maintenance process that scans the stream for stale or orphaned messages and reads them directly rather than through a consumer. On a workqueue stream this is often the more practical approach, precisely because of the non-overlapping filter constraint.
  • Application-level routing that publishes unknown or unsupported work to a separate subject — and therefore a separate consumer or stream — before it can become orphaned.
  • Consumer logic that republishes failed work to a dead-letter subject after it decides the message cannot be handled.

The important point is that fallback behavior should be part of your application design, not an implicit expectation of the stream.

What about a dead-letter queue?

JetStream has consumer delivery controls, such as a maximum delivery limit, that are useful for poison messages: messages that are delivered to a consumer but repeatedly fail processing. When a consumer reaches that limit, the server stops redelivering the message to that consumer and emits an advisory event that maintenance tooling can subscribe to. Reaching the delivery limit does not by itself remove the message from a workqueue or interest stream: on those streams a message is removed only when it is acknowledged or explicitly deleted.

That is a different case from a message that has no matching consumer at all.

A conventional dead-letter queue pattern in JetStream is implemented by the application or by operational tooling. For example, after a consumer decides a message cannot be processed, it can publish the payload and useful metadata to a separate dead-letter subject or stream, then acknowledge the original message so it is removed, or otherwise stop retrying it according to the application’s rules. The max-delivery advisory can drive the same flow from outside the consumer.

For messages that were never eligible for any consumer, you need a cleanup or routing process rather than relying only on redelivery settings.

Implementing a fallback or cleanup process

If you must keep workqueue retention but want to detect stale unhandled messages, you can build a small reaper or fallback worker.

A cautious version of that process is:

  1. List the consumers on the stream.
  2. Inspect each consumer’s filter subject or subjects, if applicable.
  3. Check the stream’s state, including its first sequence, last sequence, and stored message count.
  4. Compare the stream’s oldest message sequence with the lowest relevant consumer acknowledgment floor.
  5. For candidate messages, read them directly from the stream by sequence.
  6. Verify that the message is truly stale and not covered by an active consumer you expect to resume.
  7. Either process it, republish it to a dead-letter subject, or delete it from the stream.

Be careful with this pattern. Sequence comparisons can help find suspicious leading messages, but they are not a substitute for understanding your subject filters and consumer lifecycle. A filtered consumer may legitimately skip messages on subjects it does not match, and a temporarily stopped durable consumer may still be expected to resume later. Also note that reading a message directly from the stream does not acknowledge or remove it: on a workqueue stream the message stays until it is explicitly deleted or acknowledged through a consumer, so the cleanup step must delete it deliberately.

For safety, many teams add an age threshold before cleanup. For example, only consider messages older than the maximum expected worker outage or deployment window. In stricter environments, run this process under a leader lock or during maintenance to avoid racing with normal consumers.

Use stream limits as a backstop

Even if you implement fallback processing, stream limits are an important guardrail. Consider configuring limits such as:

  • Maximum age for messages.
  • Maximum number of messages.
  • Maximum total bytes.

These limits prevent unbounded growth, but they also define when data may be removed. Use them only after deciding what loss, expiry, or replay behavior is acceptable for the workload.

For task systems, max age is often the most understandable limit: if work is not processed within a defined time window, it is no longer useful or should be handled by another operational path.

Choosing the right pattern

Use this decision guide:

RequirementBetter fit
Work must wait for a future workerWorkQueue retention
Work should disappear when there is no interestInterest retention
Failed deliveries should be isolatedApplication-level dead-letter subject or stream
Unknown or unsupported work should be handled separatelyExplicit fallback routing or cleanup worker
Stream growth must be boundedStream limits, especially max age or max bytes

Practical guidance

If your application expects no consumer means drop it, a workqueue stream is probably not the right retention model. Interest retention may better match that behavior, provided you manage durable consumers carefully.

If your application expects every task to be durable until processed, keep workqueue retention, but add operational safeguards: stream limits, monitoring, and an explicit process for stale messages. If you need a dead-letter queue, implement it as a clear application-level path rather than expecting JetStream to automatically move messages with no matching consumer.


Want help from the NATS experts? Meet with our architects to get help tailored to your use case and environment.

Get the NATS Newsletter

News and content from across the community


Cancel