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.
With a durable pull consumer, multiple clients can fetch from the same consumer. In that configuration, the consumer distributes work across those clients:
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.
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:
| Goal | JetStream model |
|---|---|
| Multiple workers share work | One pull consumer used by multiple clients, or a push consumer with a deliver group |
| Every client receives every message | One consumer per client |
| Proceed only after every client processed a message | Track completion across those consumers or add application-level acknowledgements |
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-amskeys-edge-sfokeys-edge-sinkeys-edge-iadEach 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.
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:
In pseudocode, the gate is:
1for each expected edge consumer:2 info = get consumer info3 require info.ack_floor.stream_seq >= key_update_stream_sequence4
5if every expected consumer satisfies the check:6 enable the new signing key7else:8 wait, alert, retry, or follow an operational override pathThis 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:
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:
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.
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:
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.
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.



News and content from across the community