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 whether multiple clients attached to one JetStream durable consumer each need to acknowledge the same message before the consumer’s ack floor advances.

The short answer: no. A JetStream consumer represents a delivery and acknowledgement state machine. If multiple clients use the same pull consumer, they share that consumer’s work. They do not each receive and acknowledge every message.

One consumer shared by many clients means load balancing

With a durable pull consumer, multiple clients can fetch from the same consumer. In that configuration, the consumer distributes work across those clients:

  • client A may receive message 1
  • client B may receive message 2
  • client C may receive message 3

When the client that received a message acknowledges it, that acknowledgement updates the state for the shared consumer. The message is not waiting for acknowledgements from every other client attached to that same consumer.

The consumer’s ack floor is the consumer-level acknowledgement position. It is not a per-subscriber quorum.

Where queue groups fit in

This area causes confusion because NATS also has queue groups.

Queue groups are a core NATS feature that predates JetStream. Multiple subscribers join a named group on a subject, and the server delivers each message to only one member of that group. They are how you load balance plain, non-JetStream subscribers.

JetStream pull consumers do not use queue groups. The pull protocol already lets multiple clients share one consumer’s work by fetching from the same consumer, so load balancing is built into the consumer itself.

Queue groups become relevant for JetStream only with a push consumer. A push consumer delivers messages to a subject, and to load balance that delivery across several subscribers you configure a deliver group (deliver_group) on the consumer — the deliver subject is then consumed as a queue group. A push consumer without a deliver group is intended for a single subscriber. Pointing several plain subscribers at one push consumer’s delivery subject is a misconfiguration: even if it appears to work in a simple test, it does not provide an “all subscribers must ack” barrier.

A useful rule of thumb:

GoalJetStream model
Multiple workers share workOne pull consumer used by multiple clients, or a push consumer with a deliver group
Every client receives every messageOne consumer per client
Proceed only after every client processed a messageTrack completion across those consumers or add application-level acknowledgements

If every client must receive every message, use one consumer per client

If ten clients must each receive and process every message, create ten consumers. Each consumer has its own delivery state, pending state, redelivery behavior, and ack floor.

For example, if you have edge services that must all receive a key-set update:

  • keys-edge-ams
  • keys-edge-sfo
  • keys-edge-sin
  • keys-edge-iad

Each edge service reads from its own durable consumer. The edge acknowledges the update only after it has safely applied the new public key set.

That gives you a concrete thing to check: each consumer’s position.

Coordinating a key rotation using ack floors

Consider a key rotation workflow where a central issuer should not start signing with a new private key until every edge has received the corresponding public verification keys.

A practical JetStream-based pattern is:

  1. Create one durable consumer per edge or validation node.
  2. Publish the new public key set to the stream.
  3. Record the published stream sequence for that key-set message.
  4. Have each edge process the message and acknowledge it only after the new keys are installed.
  5. Before the issuer starts using the new signing key, inspect every expected consumer and verify that its ack floor has reached the key-set message sequence.

In pseudocode, the gate is:

1
for each expected edge consumer:
2
info = get consumer info
3
require info.ack_floor.stream_seq >= key_update_stream_sequence
4
5
if every expected consumer satisfies the check:
6
enable the new signing key
7
else:
8
wait, alert, retry, or follow an operational override path

This is often a better fit than trying to make several subscribers share one consumer. The consumers are the independent processing positions, so checking each consumer’s ack floor matches the operational requirement.

A few details matter:

  • Use a stable source of truth for the expected edge consumers. Do not accidentally treat a missing consumer as success.
  • Prefer a dedicated stream or carefully designed subjects and filters so the sequence comparison means what you intend.
  • Remember that the ack floor is contiguous. If an earlier delivered message is not acknowledged, the floor may not advance past it.
  • Decide what should happen when an edge is offline: wait, fail closed, alert an operator, use a grace period, or apply a manual emergency process.

Is there an advisory for “all clients acked”?

There is no single shared-consumer event that means “all clients attached to this consumer have acknowledged this message,” because that is not how a JetStream consumer works.

For an exact coordination point, use one of these approaches:

  • inspect each consumer’s state through the management/API path your application already uses;
  • publish explicit application-level acknowledgements from each participant to a separate subject or stream;
  • combine both, using JetStream acknowledgement state for delivery and application acknowledgements for domain-specific readiness.

Operational events and sampled acknowledgement metrics can be useful for observability, but they should not be treated as a durable completion barrier unless your system is explicitly designed and tested around their semantics.

What about an interest-based stream with MaxMsgs: 1?

There is a niche pattern where an interest-retention stream, MaxMsgs: 1, and a discard policy such as DiscardNew can make new publishes fail while an existing message is still retained. In some designs, that can approximate a “do not accept the next item until the current item is gone” workflow.

That tradeoff is usually restrictive:

  • only one retained message can exist at a time;
  • new writes may be rejected while consumers are still processing;
  • throughput and operational behavior may be poor for general fan-out workflows;
  • it still needs careful testing against your exact retention, consumer, and failure semantics.

For a key rotation that happens infrequently, it may be tempting, but a per-edge durable consumer plus an explicit readiness check is usually easier to reason about.

Practical guidance

Use a shared consumer when you want workers to split work. Use separate consumers when each participant must independently process every message.

For coordinated fan-out, such as ensuring every edge has installed new verification keys before a signer starts minting tokens with a new key, model each edge as its own durable consumer and gate the next step on each consumer’s ack floor or on explicit application-level readiness messages.


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