Guaranteeing At‑Least‑Once Delivery in NATS JetStream with Durable Subscriptions
Learn how JetStream’s durable subscriptions enable true at‑least‑once delivery, the trade‑offs involved, and a step‑by‑step test you can run today.
05 Nov 2025, 17:55 UTC

Problem: How can a NATS client keep receiving messages even after a crash?
In many distributed systems, a consumer may restart or lose network connectivity. If the message broker simply pushes a message once, a restart could mean that message is lost forever. NATS JetStream solves this with durable subscriptions that persist the consumer state on disk, enabling the broker to replay missed messages when the client reconnects.
Thesis: Durable consumers with the ack_policy=explicit setting give you a true at‑least‑once guarantee.
When a consumer acknowledges a message, JetStream removes it from the replay queue. If the consumer fails to acknowledge before the ack_wait period, JetStream will redeliver it. Because the consumer name is persisted, the same logical consumer will pick up where it left off after a restart, regardless of the client ID.
Key Concepts
- Stream – a log of messages that JetStream stores on disk.
- Consumer – a subscription that pulls messages from a stream. A durable consumer keeps its state on disk.
- Ack policy – controls when a consumer must acknowledge.
explicitmeans the consumer must callMsg.Ack()to confirm receipt. - At‑least‑once – the broker guarantees that each message will be delivered at least once, but duplicates can occur if a consumer dies before acknowledging.
Concrete Example: Test the Workflow End‑to‑End
We’ll use the nats CLI to demonstrate the flow. Replace <STREAM> and <CONSUMER> with your own identifiers.
# 1. Create a stream that holds 100 messages for 1 day
nats stream add <STREAM> --subjects "demo.>" --retention limits --max-msgs 100 --max-age 1d
# 2. Publish a test message
nats pub demo.test "Hello, JetStream!"
# 3. Subscribe with a durable consumer, at‑least‑once ack policy
# The consumer name <CONSUMER> is stored on disk.
# The client ID can be anything; the durable name is what matters.
# Run this in a terminal window and keep it open.
# (Use a unique client ID to avoid conflicts.)
nats sub durable <CONSUMER> --stream <STREAM> --ack --ack-policy explicit
# 4. Observe the message appear in the terminal.
# Press Ctrl+C to exit the subscription.
# 5. Re‑run the subscription to see replay.
# Since the consumer was durable and the message was not acknowledged,
# JetStream will redeliver the message.
nats sub durable <CONSUMER> --stream <STREAM> --ack --ack-policy explicit
After step 5 you should see the same "Hello, JetStream!" message again. That demonstrates the replay capability.
Trade‑offs and Limitations
- Disk Footprint – Durable consumers retain the entire message history that has not been acknowledged. In high‑throughput scenarios, this can grow quickly.
- Replaying Large Histories – JetStream will replay all unacknowledged messages on startup. If a consumer has been down for a long time, the initial backlog may delay the first useful message.
- Consumer Name Conflicts – Using the same durable name across multiple clients (different client IDs) will cause a conflict. JetStream will reject the second subscriber with a
ConsumerNameInUseerror. Keep durable names unique per logical consumer. - Duplicate Messages – At‑least‑once guarantees duplicates are possible if a consumer fails before acknowledging. Your application must be idempotent or handle duplicates.
Practical Checklist
- Define a clear naming convention for streams and durable consumers.
- Set
ack_policy=explicitandack_waitto a reasonable value (e.g., 30s). - Monitor the
DeliveredandPendingcounters vianats consumer infoto detect backlog growth. - Periodically prune old messages with
max_ageormax_msgsstream limits. - Implement idempotent message processing to safely handle duplicates.
Actionable Closing
Start by creating a small stream and a durable consumer in your dev environment. Use the CLI example above to confirm replay works. Once you’re comfortable, scale the stream size and observe how the consumer backlog behaves. Remember to keep your consumer names unique and your application tolerant of duplicates. With these steps, you’ll have a production‑ready at‑least‑once delivery pipeline on NATS JetStream.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.