Implementing Reliable Message Processing with NATS JetStream Pull Consumers
Learn how to prevent slow consumer issues in NATS by implementing JetStream Pull Consumers with explicit acknowledgments for reliable, at-least-once delivery.
01 Nov 2025, 20:44 UTC

Solving the Slow Consumer Problem
In standard NATS pub-sub, messages are fire-and-forget. If a subscriber cannot keep up with the volume of incoming data, the server eventually drops messages to protect its own memory—a scenario known as the "slow consumer" problem. To ensure no data is lost and that processing happens at a controlled rate, you must use JetStream with Pull Consumers.
The core takeaway is this: Use Pull Consumers when your processing logic is computationally expensive or variable in speed. Instead of the server pushing data at you, your application requests a specific batch size only when it has the available resources to process them.
How Pull Consumers Work
JetStream adds a persistence layer to NATS. While a Stream stores the messages on disk or memory, a Consumer acts as a pointer or a view into that stream. A Pull Consumer does not maintain an active connection to push data; instead, it waits for the client to send a FETCH request.
To guarantee that a message is successfully processed, you should use AckExplicit. In this mode, the server keeps the message marked as "delivered" but not "acknowledged." If the client fails to send an acknowledgement within a configured window, the server redelivers the message.
Configuration Example: Durable Pull Consumer
This example demonstrates how to set up a stream and a durable pull consumer using the NATS CLI. This assumes you have a NATS server running with the -js flag enabled.
1. Create the Stream
Run this command on your terminal to create a stream that captures all messages on subjects starting with orders.*.
nats stream add ORDERS --subjects "orders.*" --storage file --retention limits
Risk: Using --storage file ensures persistence across restarts but requires available disk space. If the disk fills, the stream will stop accepting new messages.
2. Create the Durable Pull Consumer
Create a consumer named processor-1. We will configure it to require explicit acknowledgements and set a redelivery timeout of 30 seconds.
nats consumer add ORDERS processor-1 --pull --ack explicit --ack-wait 30s
Key Parameters:
--pull: Tells the server not to push messages to the client.--ack explicit: Requires the client to callmsg.Ack()for every single message.--ack-wait 30s: If the server doesn't receive an Ack within 30 seconds, it assumes the client crashed and redelivers the message.
3. Client-Side Fetch Logic (Conceptual)
When implementing the client in a language like Go or Python, the logic follows this loop:
- Request a batch (e.g., 10 messages) from the
processor-1consumer. - Iterate through the batch and perform the business logic.
- Call
msg.Ack()only after the business logic completes successfully. - If an error occurs, call
msg.Nak()(Negative Acknowledgement) to trigger immediate redelivery.
Comparison: Pull vs. Push Consumers
| Feature | Push Consumer | Pull Consumer |
|---|---|---|
| Delivery Trigger | Server-initiated (Immediate) | Client-initiated (On-demand) |
| Flow Control | Server-side buffering/drops | Client-side backpressure |
| Latency | Lowest possible | Slightly higher (due to fetch request) |
| Best Use Case | Real-time dashboards, alerts | Batch processing, database writes |
Critical Limitations and Common Pitfalls
The AckWait Trap
A common mistake is setting --ack-wait too low. If your processing takes 40 seconds but your ack-wait is 30 seconds, the server will redeliver the message while the first client is still working on it. This leads to duplicate processing and potential data corruption. Always set ack-wait to the maximum expected processing time plus a safety margin.
Ephemeral vs. Durable Consumers
If you create consumers without a name (ephemeral), NATS deletes them automatically after a period of inactivity. For production workloads where you need to resume exactly where you left off after a restart, you must use named Durable Consumers.
Resource Exhaustion
Avoid creating a unique consumer for every single client instance if those clients are short-lived. Each durable consumer consumes server memory to track the state of acknowledged messages. Use a shared queue group or a limited set of durable consumers to maintain server stability.
Verifying the Implementation
To verify that your consumer is working and tracking state, use the NATS CLI to inspect the consumer's health:
nats consumer info ORDERS processor-1
Check the Num Pending and Num Ack Pending fields. If Num Ack Pending grows indefinitely, your clients are receiving messages but failing to call Ack(), which will eventually lead to a massive wave of redeliveries once the ack-wait timer expires.
Rollback Procedure
If a consumer configuration is incorrect (e.g., wrong ack-wait), you cannot modify a durable consumer's core settings in place. You must delete and recreate it:
- Stop the client applications to prevent processing during the transition.
- Delete the consumer:
nats consumer rm ORDERS processor-1. - Re-run the
nats consumer addcommand with the corrected parameters.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.