NATS durable consumers can replay messages on reconnect, but they don’t do it automatically by default; you have to explicitly enable and configure it.

Let’s see NATS durable consumers in action. Imagine a simple scenario: a service data-processor needs to reliably consume messages from a subject sensor.updates that might be published by a sensor-publisher. If the data-processor restarts, we want it to pick up where it left off, not miss any messages.

Here’s how you’d set up a durable consumer in NATS JetStream.

First, on the NATS server (or within your application if using the NATS client libraries), you need to create a stream to hold the messages.

nats stream create sensor_updates --subjects sensor.updates --storage file --replicas 1

This command creates a stream named sensor_updates that will capture all messages published to sensor.updates. We’re using file storage for simplicity and setting up one replica.

Now, let’s simulate a consumer that needs to be durable. We’ll use the nats CLI for this, but the principle applies to any NATS client library.

nats consumer add sensor_updates data_processor_consumer --durable data_processor_consumer --ack explicit --deliver-all

Let’s break this down:

  • nats consumer add sensor_updates: This tells NATS we’re adding a consumer to the sensor_updates stream.
  • data_processor_consumer: This is the name of our consumer.
  • --durable data_processor_consumer: This is the crucial part. It designates this consumer as "durable." NATS will store the consumer’s state (which messages it has acknowledged) persistently.
  • --ack explicit: We’re telling the consumer that it needs to explicitly acknowledge each message after processing it. This is essential for reliability.
  • --deliver-all: This is a delivery policy. deliver-all means that when this durable consumer is first created, it will receive all messages from the beginning of the stream. Subsequent reconnections will resume from the last acknowledged message.

Now, let’s publish some messages and then simulate a consumer restart.

First, publish a few messages:

nats pub sensor.updates "temp:25"
nats pub sensor.updates "humidity:60"
nats pub sensor.updates "pressure:1012"

Next, start the consumer to process these messages. We can use the nats consume command for demonstration.

nats consume sensor.updates --consumer data_processor_consumer --filter 'durable:data_processor_consumer' --wait 5

You’ll see output like this as it processes the messages. Crucially, it will acknowledge them implicitly if you don’t use --ack explicit or if you manually acknowledge them. For this example, let’s assume it acknowledges them.

Now, imagine the data-processor application crashes or is restarted. When it comes back online, it will try to connect to NATS and resume consumption. Because we specified --durable data_processor_consumer, NATS knows about this consumer’s state.

When the consumer reconnects, NATS will look up the data_processor_consumer’s state. If it has already acknowledged temp:25, humidity:60, and pressure:1012, it will simply wait for new messages.

However, what if the consumer didn’t acknowledge the messages before it crashed? This is where durability shines. When the durable consumer reconnects, NATS will redeliver the unacknowledged messages. The consumer then has another chance to process them and acknowledge them.

The "replay on reconnect" aspect is intrinsically tied to the consumer’s acknowledgment status. If a durable consumer disconnects and then reconnects, NATS will resume delivering messages from the point after the last acknowledged message. If messages were published while the consumer was offline, those messages will be available for delivery. The key is that the consumer’s progress is persisted.

The deliver-all policy is useful for initial setup or for consumers that need to see everything, but for typical "resume where you left off" scenarios after a crash, you’d often use deliver-new (the default) or deliver-last.

If you want explicit control over when a replay happens, beyond just reconnecting, you can use the replay API. For instance, you could tell a durable consumer to go back and re-process messages from a specific time or sequence number. This is often done via API calls to the JetStream server, not typically through the basic nats consume command.

Consider a scenario where you want to reprocess the last 100 messages for some reason. You’d typically interact with the JetStream API to reset the consumer’s state or trigger a replay. The CLI can help with some aspects, but for fine-grained control, you’d use a client library.

The mental model to hold onto is that a durable consumer is a named consumer whose state (acknowledgment position, last delivered message sequence, etc.) is stored by JetStream. When it reconnects, JetStream looks up this state and resumes delivery. If the consumer itself crashes before acknowledging a message, that message is still in the stream and will be redelivered upon reconnection because its acknowledgment status wasn’t updated. The "replay" isn’t a special mode; it’s the natural consequence of a durable consumer resuming its work from its last known good state.

The max_deliver setting on a consumer is also important. If a message is redelivered too many times (exceeding max_deliver), JetStream can be configured to move that message to a Dead Letter Queue (DLQ) or discard it, preventing infinite redelivery loops for problematic messages.

The next concept you’ll likely run into is configuring Dead Letter Queues (DLQs) for durable consumers that fail to process messages after multiple retries.

Want structured learning?

Take the full Nats course →