A common community question is how to process JetStream messages with multiple application instances while preventing races for stateful updates, such as applying several updates for the same user in the exact order they were published.
The short answer: if you need one-at-a-time processing for a JetStream consumer, set MaxAckPending to 1 and have your workers share that same consumer. This serializes delivery for that consumer, so only one message is in flight at a time, but it also makes that consumer a throughput bottleneck. Pair it with idempotent processing, because a message can still be redelivered. To scale while preserving order, partition the work so each key is serial, but different keys can run in parallel.
In JetStream, a consumer is server-side state: it tracks delivery position, acknowledgements, redeliveries, and related configuration. Application instances that fetch or subscribe are workers or client subscribers.
That distinction matters:
If the goal is to have ConsumerA and ConsumerB divide work without duplicating it, they should share the same JetStream consumer rather than each using a separate durable consumer for the same filter.
MaxAckPending limits how many messages may be outstanding, meaning delivered but not yet acknowledged, for a consumer.
When MaxAckPending is set to 1, JetStream will not allow that consumer to have a second unacknowledged message in flight. In practice, this means:
For a stream containing messages like:
1msgs.received_from.alice { set_age: 50 years }2msgs.received_from.alice { set_age: 35 years }3msgs.received_from.alice { set_age: 70 years }A shared consumer filtered to msgs.received_from.* with MaxAckPending=1 will process those matching messages one at a time. If one worker receives the first update, another worker cannot receive the second update from that same consumer until the first message has been acknowledged.
This is the simplest way to use acknowledgement as the ordering gate.
One caveat matters here: MaxAckPending=1 gates distinct messages, but it does not guarantee that a single message is processed only once. If a worker’s AckWait deadline elapses before it acknowledges — because processing is slow or the ack is lost — JetStream redelivers that same message. With multiple workers sharing the consumer, the redelivery can go to a different worker while the first is still running, so both process the same message at once.
To keep processing genuinely safe:
AckWait comfortably above your worst-case processing time.Put differently: MaxAckPending=1 serializes the stream of new messages, while a sane AckWait and idempotent side effects cover the redelivery edge case.
The important downside is that MaxAckPending=1 applies to the consumer, not just to one subject token or one logical entity.
If the consumer filters msgs.received_from.*, then all matching messages are effectively processed one at a time:
1msgs.received_from.alice2msgs.received_from.bob3msgs.received_from.carolEven though Alice, Bob, and Carol might be independent, a single consumer with MaxAckPending=1 serializes all of them. Multiple workers can still provide resilience because another worker can continue after a failure or redelivery, but they do not increase concurrent processing for that consumer.
So yes: this pattern gives ordered processing, but it does not scale throughput for that consumer. It mainly gives you failover and operational resilience: if a worker stops, another picks up the redelivered message, and at any moment only one message from that consumer is being worked.
JetStream client libraries may expose an ordered consumer helper for ordered message delivery. That solves a different problem: it gives a single client a fast, gap-free, in-order replay of a stream. An ordered consumer is ephemeral, bound to one subscriber, and does not use acknowledgements; it recreates itself and resets position if it detects a gap. Because it has no acknowledgement step and no shared-worker model, it cannot gate side-effecting work spread across multiple workers.
For side-effecting work across multiple workers, the key control is still the acknowledgement boundary. If the requirement is that the next database update must not begin until the previous one is complete and acknowledged, use an explicit consumer configuration such as MaxAckPending=1 and design around its throughput implications.
Most systems do not actually need one global serial lane. They need serial processing per key.
For example:
That is a partitioning problem. The usual design is:
MaxAckPending=1 per partition consumer.For example, instead of one broad lane for all senders, you might publish or route messages into partitioned subjects such as:
1msgs.received_from.p00.alice2msgs.received_from.p01.bob3msgs.received_from.p00.carolThen create consumers for partition filters such as:
1msgs.received_from.p00.*2msgs.received_from.p01.*Each partition consumer can use MaxAckPending=1, preserving order within that partition while allowing different partitions to make progress independently.
You do not necessarily have to compute the partition token in every publisher. NATS server supports deterministic subject mapping, including a built-in partitioning function that maps a chosen subject token onto a fixed number of partitions. Configured as an account subject mapping, it routes messages to partitioned subjects consistently, so the partitioning logic lives in one place instead of in every producer.
This does not make a single hot key parallel. If Alice has a very high volume of updates and those updates must be strictly ordered, Alice remains limited by one serial lane. That is inherent in the requirement.
Another way to reduce ordering pressure in the messaging layer is to make updates idempotent and versioned.
For state changes, consider including an application-level sequence, version, timestamp, or stream-derived ordering marker in the message, and have the database apply updates conditionally. For example, the database can reject an update if it is older than the version already stored.
This can be a better fit when:
This does not remove the need to think about ordering, but it moves part of the correctness guarantee to the state store, where compare-and-set or transaction semantics may already exist.
If workers must coordinate dynamically around a key, a distributed lock or lease can be used. NATS Key/Value can provide a simple building block for this: a conditional create operation succeeds only when a key does not already exist, which can be used to claim a lock-like key.
That approach has costs and caveats:
Locks can be appropriate, but they are not a generic, efficient replacement for a good partitioning model.
Use the simplest design that matches the real ordering requirement:
| Requirement | Suggested approach |
|---|---|
| Every matching message must be processed globally one at a time | One shared durable consumer with MaxAckPending=1 |
| Messages must be ordered per entity, but entities are independent | Partition by entity key; use one serial consumer per partition |
| Workers may race, but stale writes must not win | Add versions or sequence checks in the database |
| Dynamic coordination is unavoidable | Consider a lease or lock, and account for failure modes |
For many applications, the best answer is not global ordering. It is keyed ordering: preserve order where correctness requires it, and let unrelated keys run concurrently.
MaxAckPending=1 is the direct JetStream control for strict one-at-a-time processing on a consumer. It is useful and simple, but it serializes the whole consumer filter and therefore limits throughput.
To scale, design the subject and consumer model around the real ordering key. Keep each key, or each partition of keys, on a serial lane; run independent lanes in parallel; and make side effects idempotent or version-aware whenever possible.
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