NATS JetStream’s MaxDeliveryAttempts is actually a limit on how many times a message can be acknowledged by consumers, not how many times it’s delivered.

Let’s see it in action. Imagine a simple JetStream stream and a consumer that’s intentionally failing to acknowledge messages.

// Publisher side
js.Publish("my-stream", []byte("message 1"))
js.Publish("my-stream", []byte("message 2"))

// Consumer side (simplified, imagine this is in a loop)
sub, err := js.Subscribe("my-stream", func(msg *nats.Msg) {
	log.Printf("Received message: %s", msg.Data)
	// Intentionally not acknowledging to trigger MaxDeliveryAttempts
})
defer sub.Unsubscribe()

When you configure MaxDeliveryAttempts on a consumer, you’re setting a threshold. Once a message has been delivered to a consumer and that consumer has failed to acknowledge it (either by explicit Ack or Nak with delay) for MaxDeliveryAttempts times, JetStream considers the message "dead" for that consumer.

Here’s how it works internally: JetStream tracks the delivery attempts for each message per consumer. When a consumer receives a message, it’s marked as "in-flight" for that consumer. If the consumer doesn’t acknowledge it within a certain timeframe (or if it sends a Nak without a delay), JetStream will redeliver it. This redelivery count is what MaxDeliveryAttempts monitors. Once the count for a specific message and consumer pair reaches the configured MaxDeliveryAttempts, JetStream stops trying to deliver that message to that specific consumer.

The primary problem this solves is preventing message loops or preventing a single problematic message from endlessly consuming resources by being redelivered to a consumer that can’t process it. It ensures that eventually, messages are either acknowledged, sent to a Dead Letter Queue (DLQ) if configured, or discarded.

When you create or update a consumer, you set this value. For example, using the NATS CLI:

nats consumer add my-stream my-consumer --max-deliver 5

This command tells JetStream that for the consumer named my-consumer on the stream my-stream, any message that fails to be acknowledged 5 times will no longer be redelivered to this consumer.

The exact levers you control are the MaxDeliveryAttempts (or --max-deliver in the CLI) setting on the consumer configuration. You can set this to any positive integer. A value of 1 means the message will only be attempted once; if not acknowledged, it’s considered undeliverable by this consumer. Setting it to 0 means there’s effectively no limit imposed by this specific setting, though other JetStream internal limits might still apply.

What most people don’t realize is that MaxDeliveryAttempts is per-consumer. If you have multiple consumers subscribed to the same stream, a message hitting its MaxDeliveryAttempts for consumer A doesn’t affect consumer B. Consumer B will still receive and attempt to process that same message independently. This allows for robust error handling where one failing consumer doesn’t bring down the processing for others.

The next concept you’ll likely encounter is configuring a Dead Letter Queue (DLQ) to capture messages that have exhausted their delivery attempts.

Want structured learning?

Take the full Nats course →