NATS JetStream Explained: Streams, Consumers, and Durable Messaging
Core NATS is a fast, fire-and-forget message bus: publish a message to a subject, and any subscriber currently connected receives it. If nobody is subscribed, the message is gone. That is perfect for request/reply and live dashboards, and useless for anything that must survive a disconnect.
JetStream is NATS' built-in persistence layer. It turns the same subject-based pub/sub into a durable message store with replay, acknowledgements, and per-consumer progress tracking. This guide walks through the two concepts you actually need to understand (streams and consumers), then covers retention, ack semantics, and how to decide what belongs in JetStream versus Core NATS.
Streams: The Durable Message Store
A stream is a server-side store of messages, bound to one or more subjects. When you publish to a subject a stream is bound to, the server persists the message instead of just broadcasting it to current subscribers.
Publish → orders.created → Stream "ORDERS"
orders.fulfilled → ├── msg 1 (orders.created)
orders.cancelled → ├── msg 2 (orders.created)
└── msg 3 (orders.cancelled)
Key stream settings:
- Subjects: which subjects the stream captures (wildcards allowed, e.g.
orders.>). - Storage: file-backed (durable across restarts) or memory-backed (fast, lost on restart).
- Retention policy: how long messages are kept (see below).
- Max age / max bytes / max messages: the practical limits that bound the stream.
Retention Policies
| Policy | Keeps messages while | Best for |
|---|---|---|
| Limits | Within max age/bytes/messages | Time-series, telemetry, event logs |
| Interest | At least one consumer is behind | Task queues, work distribution |
| Work queue | Until a consumer acks | Exactly-one-processing jobs |
The default is Limits: keep everything up to your configured bounds, then drop the oldest. That is the right choice for sensor data, logs, and any append-mostly workload. Work queue is the interesting one for job processing: each message is delivered to exactly one consumer and removed once acknowledged.
Consumers: The Stateful View
A consumer is a server-side, stateful view of a stream. The server tracks how far this consumer has progressed, so your application does not have to remember offsets. You can create multiple consumers over the same stream, each with its own position: one for a dashboard reading from the start, one for a worker only taking new messages.
Push vs Pull
- Push consumers: the server delivers messages to a subscription as they arrive. Good for continuous processing where the consumer can keep up.
- Pull consumers: the application asks for the next message (or a batch) when it is ready. Good for workers that want back-pressure, or for on-demand processing.
Pull consumers are the workhorse of task processing: a fleet of workers each pull the next message, process it, and ack it. The server hands out each message to only one pull consumer in the group.
Acknowledgements and Redelivery
JetStream consumers acknowledge messages. The ack tells the server "I processed this, you can move on." The ack policy controls what happens if the consumer dies or takes too long:
- Ack none: no ack expected; messages are delivered and forgotten.
- Ack explicit: the application must ack each message; unacked messages are redelivered after
ack_wait. - Ack all: acking message N also acks everything before it (batch-friendly).
Redelivery is bounded by max_deliver. When a message exceeds that, it is moved to the dead-letter subject if you configured one. Otherwise it is dropped. That is the mechanism that makes "exactly-once-ish" processing possible: at-least-once delivery plus deduplication plus bounded retries.
Mental model: Core NATS gives you at-most-once with zero overhead. JetStream gives you at-least-once with ack-based redelivery. It does not magically make processing idempotent: your handler should still be safe to run twice.
Replay and Catch-Up
Because the stream stores messages, a new consumer can replay history. You choose the delivery policy when you create the consumer:
- Deliver all: start from the oldest message in the stream.
- Deliver new: only messages published after the consumer starts.
- Deliver by sequence / start time: replay from a specific offset or timestamp.
Replay is what makes JetStream a historian, not just a bus: a downed dashboard reconnects, creates a consumer from its last sequence, and catches up on everything it missed.
JetStream vs Core NATS: What to Put Where
| Core NATS | JetStream | |
|---|---|---|
| Delivery | At-most-once, live only | Durable, ack-based, replayable |
| Latency | Lowest: no disk, no ack | Higher: persistence + acks |
| Use for | Request/reply, live telemetry, fan-out | Histories, queues, job processing, catch-up |
| Storage cost | None | Disk, retention limits to manage |
The common mistake is putting everything in JetStream "just in case." A live dashboard feeding off a stream adds disk I/O and ack overhead for data nobody will ever replay. Keep hot live data on Core NATS subjects; mirror the important subset into a stream only when you actually need history.
Getting Hands-On
The fastest way to internalize streams and consumers is to create a stream, publish to its subject, then create a couple of consumers with different delivery policies and watch each one advance independently. A GUI makes that far more tangible than typing nats CLI commands.
NATS Explorer ships an embedded NATS server with JetStream enabled. You can start the server, create a stream bound to orders.>, publish a few messages, then create a durable consumer and a pull consumer over the same stream, and watch their positions diverge as you publish more.
See JetStream Visually
NATS Explorer for macOS, Windows, and Linux includes an embedded server plus stream, consumer, KV, and Object store browsers. No CLI required.
Explore NATS Explorer →