How to enable persistent storage using NATS JetStream

How to enable persistent storage using NATS JetStream

nats_jetstream_consumer_demo

Here in this article we will try to see how we can enable JetStream feature in NATS which provides with the persistent layer on top of NATS Core. We will try also to persist messages into streams and replay them as per our convenience using consumers.

Test Environment

  • Fedora 41 server

What is JetStream

JetStream is a built-in feature of NATS Core, which needs to be enabled. JetStream allows the NATS server to capture messages and replay them to consumers as needed. This functionality enables a different quality of service for your NATS messages, and enables fault-tolerant and high-availability configurations.

JetStream is truly multi-tenant as it supports account-based security model, horizontally scalable, or supports multiple deployment models. It also provides support for storing key-value and filestore data in the streams. JetStream provides both the ability to consume messages as they are published (i.e. ‘queueing’) as well as the ability to replay messages on demand (i.e. ‘streaming’).

What are Consumers

A consumer is a stateful view of a stream. It acts as an interface for clients to consume a subset of messages stored in a stream and will keep track of which messages were delivered and acknowledged by clients. A consumer in JetStream can provide an at least once delivery guarantee.

In Summary, Streams are responsible for storing the published messages, the Consumers are responsible for tracking the delivery and acknowledgement of those messages.

Types of Consumers

  1. Dispatch Type:
    • Pull Consumer: In this type, the clients need to explicitly request batches of messages on demand
    • Push Consumer: In this type messages will be delivered to a specified subject
  2. Persistence:
    • Ephemeral: An ephemeral consumer does not persist delivery progress and will automatically be deleted when there are no more client instances connected
    • Durable: A consumer is considered durable when an explicit name is set on the Durable field when creating the consumer, or when InactiveThreshold is set

High Level Architecture

Procedure

Step1: Ensure NATS server enabled with JetStream

For a very basic demonstration we will start NATS server with JetStream enabled by passing “-js” option while starting up the NATS server.

admin@linuxser:~$ nats-server -js
[3178] 2025/07/22 17:06:40.620294 [INF] Starting nats-server
[3178] 2025/07/22 17:06:40.620543 [INF]   Version:  2.10.20
[3178] 2025/07/22 17:06:40.620546 [INF]   Git:      [7140387]
[3178] 2025/07/22 17:06:40.620549 [INF]   Name:     NA3QJJABLSUUVGJRO2PFWUWJ46XXNK7NTMUUAV2MYBV5WQGUQVYSOOZL
[3178] 2025/07/22 17:06:40.620553 [INF]   Node:     EyHo2pHI
[3178] 2025/07/22 17:06:40.620555 [INF]   ID:       NA3QJJABLSUUVGJRO2PFWUWJ46XXNK7NTMUUAV2MYBV5WQGUQVYSOOZL
[3178] 2025/07/22 17:06:40.620999 [INF] Starting JetStream
[3178] 2025/07/22 17:06:40.621464 [INF]     _ ___ _____ ___ _____ ___ ___   _   __  __
[3178] 2025/07/22 17:06:40.621476 [INF]  _ | | __|_   _/ __|_   _| _ \ __| /_\ |  \/  |
[3178] 2025/07/22 17:06:40.621479 [INF] | || | _|  | | \__ \ | | |   / _| / _ \| |\/| |
[3178] 2025/07/22 17:06:40.621482 [INF]  \__/|___| |_| |___/ |_| |_|_\___/_/ \_\_|  |_|
[3178] 2025/07/22 17:06:40.621485 [INF] 
[3178] 2025/07/22 17:06:40.621487 [INF]          https://docs.nats.io/jetstream
[3178] 2025/07/22 17:06:40.621490 [INF] 
[3178] 2025/07/22 17:06:40.621493 [INF] ---------------- JETSTREAM ----------------
[3178] 2025/07/22 17:06:40.621503 [INF]   Max Memory:      2.86 GB
[3178] 2025/07/22 17:06:40.621509 [INF]   Max Storage:     1.43 GB
[3178] 2025/07/22 17:06:40.621516 [INF]   Store Directory: "/tmp/nats/jetstream"
[3178] 2025/07/22 17:06:40.621523 [INF] -------------------------------------------
[3178] 2025/07/22 17:06:40.622373 [INF] Listening for client connections on 0.0.0.0:4222
[3178] 2025/07/22 17:06:40.622859 [INF] Server is ready

We can also verify if JetStream is enabled on a server by running the following command. If you can find “JetStream Account Information” with the details its enabled.

admin@linuxser:~$ nats account info

Step2: Create a Stream

A stream is used to capture and store the messages that are published on a particular subject. Let’s create a stream named “python_articles” for the messages on subject “hello.python”.

admin@linuxser:~$ nats stream add python_articles
[localhost] ? Subjects hello.python
[localhost] ? Storage file
[localhost] ? Replication 1
[localhost] ? Retention Policy Limits
[localhost] ? Discard Policy Old
[localhost] ? Stream Messages Limit -1
[localhost] ? Per Subject Messages Limit -1
[localhost] ? Total Stream Size -1
[localhost] ? Message TTL -1
[localhost] ? Max Message Size -1
[localhost] ? Duplicate tracking time window 2m0s
[localhost] ? Allow message Roll-ups No
[localhost] ? Allow message deletion Yes
[localhost] ? Allow purging subjects or the entire stream Yes
Stream python_articles was created

Once the stream has been created you can verify that it gets persisted under the “/tmp/nats/jetstream” jetstream store directory as shown below.

admin@linuxser:~$ ls -ltr /tmp/nats/jetstream/\$G/streams/python_articles/
total 8
drwxr-x---. 2 admin admin  40 Jul 22 17:14 obs
-rw-r-----. 1 admin admin  16 Jul 22 17:14 meta.sum
-rw-r-----. 1 admin admin 468 Jul 22 17:14 meta.inf
drwxr-x---. 2 admin admin  80 Jul 22 17:16 msgs

You can now list the stream that we just created and also get more information using the below commands.

admin@linuxser:~$ nats stream ls
admin@linuxser:~$ nats stream info python_articles

Step3: Publish messages to Stream

Let’s now publish few articles on our subject “hello.python” using “nats pub”.

admin@linuxser:~$ nats pub hello.python "Introduction to Python"
17:21:42 Published 22 bytes to "hello.python"
admin@linuxser:~$ nats pub hello.python "Python DataTypes"
17:22:02 Published 16 bytes to "hello.python"
admin@linuxser:~$ nats pub hello.python "Python Functions"
17:22:17 Published 16 bytes to "hello.python"
admin@linuxser:~$ nats pub hello.python "Python Classes"
17:22:23 Published 14 bytes to "hello.python"
admin@linuxser:~$ nats pub hello.python "Python Libraries"
17:22:29 Published 16 bytes to "hello.python"

Now that we enable jetstream and created our stream “python_articles” which is mapped to our subject “hello.python”. All the messages that are published will be stored in the stream as shown below.

admin@linuxser:~$ nats stream ls
╭───────────────────────────────────────────────────────────────────────────────────────╮
│                                        Streams                                        │
├─────────────────┬─────────────┬─────────────────────┬──────────┬───────┬──────────────┤
│ Name            │ Description │ Created             │ Messages │ Size  │ Last Message │
├─────────────────┼─────────────┼─────────────────────┼──────────┼───────┼──────────────┤
│ python_articles │             │ 2025-07-22 17:14:48 │ 5        │ 294 B │ 28.00s       │
╰─────────────────┴─────────────┴─────────────────────┴──────────┴───────┴──────────────╯

If you want to look at all the message that are published into the stream, you can use the below command.

admin@linuxser:~$ nats stream view python_articles
[1] Subject: hello.python Received: 2025-07-22 17:21:42
Introduction to Python


[2] Subject: hello.python Received: 2025-07-22 17:22:02
Python DataTypes


[3] Subject: hello.python Received: 2025-07-22 17:22:17
Python Functions


[4] Subject: hello.python Received: 2025-07-22 17:22:23
Python Classes


[5] Subject: hello.python Received: 2025-07-22 17:22:29
Python Libraries

If you want to look at a specific sequence number message, you can use the below command.

admin@linuxser:~$ nats stream get python_articles
[localhost] ? Message Sequence to retrieve 2
Item: python_articles#2 received 2025-07-22 11:52:02.409925675 +0000 UTC (6m8s) on Subject hello.python

Python DataTypes

Now we have any subscriber subscribed to our subject “hello.python” those message will be stored in the stream “python_articles” and also delivered to the active subscriber. But the old messages that were published will not be delivered.

admin@linuxser:~$ nats sub "hello.python"
17:32:08 Subscribing on hello.python 
[#1] Received on "hello.python"
Python for Machine Learning
admin@linuxser:~$ nats pub hello.python "Python for Machine Learning"
17:32:11 Published 27 bytes to "hello.python"

Step4: Create a Pull Consumer

A consumer is like a view of the messages that are available in the stream. A pull consumer is basically a durable consumer as we provide consumer name. Also as we just want to pull the data from the consumer and process it rather then publish it to another client subscribed on a subject we leave the delivery target as emptly.

Let’s create a consumer named “pull_consumer”. We leave the delivery target empty for pull consumers and select all for the start policy, and can then just use the defaults and hit return for all the other prompts.

admin@linuxser:~$ nats consumer add
[localhost] ? Select a Stream python_articles
[localhost] ? Consumer name pull_consumer
[localhost] ? Delivery target (empty for Pull Consumers) 
[localhost] ? Start policy (all, new, last, subject, 1h, msg sequence) all
[localhost] ? Acknowledgment policy explicit
[localhost] ? Replay policy instant
[localhost] ? Filter Stream by subjects (blank for all) 
[localhost] ? Maximum Allowed Deliveries -1
[localhost] ? Maximum Acknowledgments Pending 0
[localhost] ? Deliver headers only without bodies No
[localhost] ? Add a Retry Backoff Policy No
Information for Consumer python_articles > pull_consumer created 2025-07-22 17:46:27

Let’s now list all the consumers that are available for our stream “python_articles”.

admin@linuxser:~$ nats consumer ls
[localhost] ? Select a Stream python_articles
╭───────────────────────────────────────────────────────────────────────────────────────────────╮
│                                           Consumers                                           │
├───────────────┬─────────────┬─────────────────────┬─────────────┬─────────────┬───────────────┤
│ Name          │ Description │ Created             │ Ack Pending │ Unprocessed │ Last Delivery │
├───────────────┼─────────────┼─────────────────────┼─────────────┼─────────────┼───────────────┤
│ pull_consumer │             │ 2025-07-22 17:46:27 │           0 │           7 │ never         │
╰───────────────┴─────────────┴─────────────────────┴─────────────┴─────────────┴───────────────╯

Step5: Consumer Messages from Pull Consumer

In NATS JetStream, creating a pull consumer with a durable name means the consumer will persist its state, allowing it to be reconnected to and continue processing messages from where it left off, even after disconnections or restarts. This is in contrast to ephemeral consumers, which are not persisted and are deleted when no longer in use.

admin@linuxser:~$ nats consumer next python_articles pull_consumer --count 10
[16:11:11] subj: hello.python / tries: 1 / cons seq: 1 / str seq: 1 / pending: 6

Introduction to Python

Acknowledged message

[16:11:11] subj: hello.python / tries: 1 / cons seq: 2 / str seq: 2 / pending: 5

Python DataTypes

Acknowledged message

[16:11:11] subj: hello.python / tries: 1 / cons seq: 3 / str seq: 3 / pending: 4

Python Functions

Acknowledged message

[16:11:11] subj: hello.python / tries: 1 / cons seq: 4 / str seq: 4 / pending: 3

Python Classes

Acknowledged message

[16:11:11] subj: hello.python / tries: 1 / cons seq: 5 / str seq: 5 / pending: 2

Python Libraries

Acknowledged message

[16:11:11] subj: hello.python / tries: 1 / cons seq: 6 / str seq: 6 / pending: 1

Python for Data Science

Acknowledged message

[16:11:11] subj: hello.python / tries: 1 / cons seq: 7 / str seq: 7 / pending: 0

Python for Machine Learning

Acknowledged message

nats: error: no message received: nats: timeout

Once you have iterated over all the messages in the stream with the consumer, you can get them again by simply creating a new consumer or by deleting that consumer (nats consumer rm) and re-creating it (nats consumer add).

Now if we list the pull_consumer again, we will see that all the messages have been processed by the consuming client process.

admin@linuxser:~$ nats consumer ls
[localhost] ? Select a Stream python_articles
╭───────────────────────────────────────────────────────────────────────────────────────────────╮
│                                           Consumers                                           │
├───────────────┬─────────────┬─────────────────────┬─────────────┬─────────────┬───────────────┤
│ Name          │ Description │ Created             │ Ack Pending │ Unprocessed │ Last Delivery │
├───────────────┼─────────────┼─────────────────────┼─────────────┼─────────────┼───────────────┤
│ pull_consumer │             │ 2025-07-22 17:46:27 │           0 │           0 │ 41.91s        │
╰───────────────┴─────────────┴─────────────────────┴─────────────┴─────────────┴───────────────╯

But the stream still persists all the messages that were published since start.

admin@linuxser:~$ nats stream ls
╭───────────────────────────────────────────────────────────────────────────────────────────╮
│                                          Streams                                          │
├─────────────────┬─────────────┬─────────────────────┬──────────┬───────┬──────────────────┤
│ Name            │ Description │ Created             │ Messages │ Size  │ Last Message     │
├─────────────────┼─────────────┼─────────────────────┼──────────┼───────┼──────────────────┤
│ python_articles │             │ 2025-07-22 17:14:48 │ 7        │ 428 B │ -1h19m31.716374s │
╰─────────────────┴─────────────┴─────────────────────┴──────────┴───────┴──────────────────╯

Step6: Create a Push Consumer

Here we will try to create a durable push consumer which can be used to publish messages to a target delivery subject by keeping track of messages that have been delivered and acknowledged.

admin@linuxser:~/nats$ nats consumer add 
[localhost] ? Select a Stream python_articles
[localhost] ? Consumer name push_consumer
[localhost] ? Delivery target (empty for Pull Consumers) hello.archive
[localhost] ? Delivery Queue Group 
[localhost] ? Start policy (all, new, last, subject, 1h, msg sequence) all
[localhost] ? Acknowledgment policy none
[localhost] ? Replay policy instant
[localhost] ? Filter Stream by subjects (blank for all) 
[localhost] ? Idle Heartbeat 0s
[localhost] ? Enable Flow Control, ie --flow-control No
[localhost] ? Deliver headers only without bodies No
Information for Consumer python_articles > push_consumer created 2025-07-25 15:34:57

Step7: Consumer Messages from Push Consumer

Now instead of pulling message from the push consumer that we just created, we just need to subcribe for message on “hello.archive” and all the messages that are avaiable through the consumer “push_consumer” will be delivered to the client instantly as soon as they are connected back.

admin@linuxser:~/nats$ nats sub "hello.archive"
15:37:14 Subscribing on hello.archive 
[#1] Received JetStream message: consumer: python_articles > push_consumer / subject: hello.python / delivered: 1 / consumer seq: 1 / stream seq: 1
Introduction to Python


[#2] Received JetStream message: consumer: python_articles > push_consumer / subject: hello.python / delivered: 1 / consumer seq: 2 / stream seq: 2
Python DataTypes


[#3] Received JetStream message: consumer: python_articles > push_consumer / subject: hello.python / delivered: 1 / consumer seq: 3 / stream seq: 3
Python Functions


[#4] Received JetStream message: consumer: python_articles > push_consumer / subject: hello.python / delivered: 1 / consumer seq: 4 / stream seq: 4
Python Classes


[#5] Received JetStream message: consumer: python_articles > push_consumer / subject: hello.python / delivered: 1 / consumer seq: 5 / stream seq: 5
Python Libraries


[#6] Received JetStream message: consumer: python_articles > push_consumer / subject: hello.python / delivered: 1 / consumer seq: 6 / stream seq: 6
Python for Machine Learning

Also if there are any new message published to the subject “hello.python”, they will be stored in the stream and consumed by the subscriber “hello.archive” through “push_consumer” in real time.

admin@linuxser:~/nats$ nats pub "hello.python" "Python for Mobile Apps"
15:39:46 Published 22 bytes to "hello.python"

In your subscriber console log you should be able to see the following message.

[#7] Received JetStream message: consumer: python_articles > push_consumer / subject: hello.python / delivered: 1 / consumer seq: 7 / stream seq: 7
Python for Mobile Apps

Step8: Consumer Message using Ephemeral Push Consumer

Ephemeral consumers are created on the fly and they get cleaned up when the client disconnects. Consumers for which we don’t provide a durable name are known as Ephemeral consumers they are created using a randomly generated string name.

Let’s have one of our client subscribed to messages on “hello.others”.

admin@linuxser:~/nats$ nats sub "hello.others"
15:47:58 Subscribing on hello.others 

Now let’s create an ephemeral push consumer which will push the messages from the consumer view to the subcriber on “hello.others”.

admin@linuxser:~/nats$ nats consumer add --ephemeral
[localhost] ? Select a Stream python_articles
[localhost] ? Delivery target (empty for Pull Consumers) hello.others
[localhost] ? Delivery Queue Group 
[localhost] ? Start policy (all, new, last, subject, 1h, msg sequence) all
[localhost] ? Acknowledgment policy none
[localhost] ? Replay policy instant
[localhost] ? Filter Stream by subjects (blank for all) 
[localhost] ? Idle Heartbeat 0s
[localhost] ? Enable Flow Control, ie --flow-control No
[localhost] ? Deliver headers only without bodies No
Information for Consumer python_articles > kSxym2wo created 2025-07-25 15:48:46

You can look at your subscriber console on “hello.others” that received all the message through this ephemeral consumer. While the client is still connected, you can list the consumer and check its status.

admin@linuxser:~/nats$ nats consumer ls
[localhost] ? Select a Stream python_articles
╭───────────────────────────────────────────────────────────────────────────────────────────────╮
│                                           Consumers                                           │
├───────────────┬─────────────┬─────────────────────┬─────────────┬─────────────┬───────────────┤
│ Name          │ Description │ Created             │ Ack Pending │ Unprocessed │ Last Delivery │
├───────────────┼─────────────┼─────────────────────┼─────────────┼─────────────┼───────────────┤
│ kSxym2wo      │             │ 2025-07-25 15:48:46 │           0 │           0 │ 1m35s         │
│ pull_consumer │             │ 2025-07-25 15:29:21 │           0 │           1 │ 20m26s        │
│ push_consumer │             │ 2025-07-25 15:34:57 │           0 │           0 │ 10m36s        │
╰───────────────┴─────────────┴─────────────────────┴─────────────┴─────────────┴───────────────╯

Now if you disconnect your subscriber on “hello.others” you will notice that the ephemeral consumer is also deleted. Ephemeral consumer can’t help in tracking message delivery status. So basically you don’t know what messages you have consumed from the stream and where to start next.

admin@linuxser:~/nats$ nats consumer ls
[localhost] ? Select a Stream python_articles
╭───────────────────────────────────────────────────────────────────────────────────────────────╮
│                                           Consumers                                           │
├───────────────┬─────────────┬─────────────────────┬─────────────┬─────────────┬───────────────┤
│ Name          │ Description │ Created             │ Ack Pending │ Unprocessed │ Last Delivery │
├───────────────┼─────────────┼─────────────────────┼─────────────┼─────────────┼───────────────┤
│ pull_consumer │             │ 2025-07-25 15:29:21 │           0 │           1 │ 22m30s        │
│ push_consumer │             │ 2025-07-25 15:34:57 │           0 │           0 │ 12m40s        │
╰───────────────┴─────────────┴─────────────────────┴─────────────┴─────────────┴───────────────╯

Step9: Clean up

We can clean up the messages in a stream using the below command.

admin@linuxser:~$ nats stream purge 
[localhost] ? Select a Stream python_articles
[localhost] ? Really purge Stream python_articles Yes

If you completely want to delete the stream and all its associated consumers we can do the following.

admin@linuxser:~$ nats stream rm
[localhost] ? Select a Stream python_articles
[localhost] ? Really delete Stream python_articles Yes

admin@linuxser:~$ nats stream ls
No Streams defined

Hope you enjoyed reading this article. Thank you..