NATS JetStream can make your message streams more persistent than you think, even when the NATS server restarts.

Let’s set up a persistent stream. Imagine a service that needs to guarantee messages are processed, even if the NATS server goes down for a bit. JetStream provides exactly that. We’re going to create a stream named ORDERS that will store messages durably.

First, we need a NATS server running with JetStream enabled. A basic nats-server command will do, but if you want to be explicit, you can use a config file like this:

# nats-server.conf
jetstream: {
  # Enable JetStream
  enabled: true
  # Store data in a directory
  store_dir: "./jetstream_data"
}

Then, start the server: nats-server -c nats-server.conf

Now, let’s create the ORDERS stream. We’ll use the NATS CLI for this. The key here is storage: file which tells JetStream to write messages to disk.

nats stream add ORDERS \
  --subjects "order.*" \
  --storage file \
  --replicas 1 \
  --retention days=7 \
  --max-age 7d \
  --max-msg-size 1MB

Let’s break down what’s happening:

  • nats stream add ORDERS: This is the command to create a new stream named ORDERS.
  • --subjects "order.*": This defines the subjects that this stream will consume messages from. In this case, any message published to a subject starting with order. (e.g., order.new, order.paid) will be stored in this stream.
  • --storage file: This is the crucial part for persistence. It tells JetStream to store the stream’s data on disk in the directory specified by store_dir in the server configuration. If you omit this, or use memory, messages would be lost on server restart.
  • --replicas 1: For high availability, you can set multiple replicas. For a single server setup, 1 is sufficient.
  • --retention days=7: This sets a policy for how long messages are kept. After 7 days, messages will be purged.
  • --max-age 7d: Similar to retention, this ensures no message is older than 7 days. This is often used in conjunction with retention policies.
  • --max-msg-size 1MB: This sets a limit on the size of individual messages that can be stored in the stream.

Once the stream is created, you can publish messages to it:

nats pub order.new '{"id": "123", "item": "widget"}'
nats pub order.new '{"id": "456", "item": "gadget"}'

And consume them:

nats sub order.new

If you stop and restart the nats-server, the ORDERS stream and its messages will still be there. This is because storage: file ensured the data was written to disk.

The most surprising thing about JetStream’s file-based persistence is that it’s not just a simple write-ahead log. JetStream uses a layered approach where new data is appended to segment files, and older segments are compacted or deleted based on your retention policies. This allows for efficient reads and writes while managing disk space. The server maintains metadata about the stream’s state, including which messages have been acknowledged by consumers, ensuring that even after a restart, consumers can resume from where they left off.

When you configure a stream with storage: file, JetStream creates a directory structure within its store_dir. Inside this, you’ll find subdirectories for each stream, containing segment files (.seg) that hold the message data and an index file (.idx) for quick lookups. The server also maintains a metadata file that tracks stream configuration, consumer information, and sequence numbers. This metadata is critical for recovering the stream’s state upon restart. The actual persistence mechanism involves appending new messages to the current segment file and then periodically creating new segments as the current one grows or ages. When a stream is deleted, these files are cleaned up.

The next step in mastering JetStream persistence is understanding how consumers interact with these durable streams and how to configure different consumer types for guaranteed delivery.

Want structured learning?

Take the full Nats course →