NATS JetStream in WSL: Streams, Consumers, and Replay Boundaries
Run a local NATS JetStream lab in WSL and distinguish ephemeral Core NATS from persisted streams, consumer acknowledgements, replay, and real cluster resilience.
NATS is a lightweight messaging system that can be useful as a local application dependency in WSL. The distinction to preserve is between Core NATS and JetStream. Core NATS delivers messages to currently interested subscribers; JetStream adds server-side persistence, streams, consumers, replay, and acknowledgement state. A service that runs successfully in a WSL distro is not an always-on message platform, and a single JetStream server does not create independent replicas.
Use a local NATS instance to test subject naming, publishing, subscriptions, consumer acknowledgements, and application recovery behavior. Keep the server, CLI, and application in the same distribution when possible. If clients run on Windows, prove the exact route and advertised endpoints from that client environment.
Enable JetStream with an explicit storage directory
The NATS server must be configured to enable JetStream. A minimal local config can name a Linux-side storage directory:
port: 4222
jetstream {
store_dir: "/home/dev/.local/share/nats/jetstream"
}
Create the parent directory for the account that runs the server and ensure it is writable by that account. Use an absolute path for a reproducible service configuration; a relative path is resolved in the server’s process context, which can vary between an interactive shell and a systemd unit. Do not put a live server’s state under a Windows-mounted project directory without measuring the filesystem behavior and understanding the consequences.
Install the NATS server and CLI from the official project instructions for the chosen Linux environment. If using systemd, inspect the package unit and service logs rather than launching an unmanaged second server:
nats-server --version
nats --version
systemctl status nats --no-pager
sudo systemctl start nats
Unit naming depends on the installation method. If the package does not install a unit, use the documented foreground or service method and record the command and config path.
Create a stream for messages that must be replayable
A Core NATS publish is not stored for a subscriber that is disconnected. A JetStream stream captures messages whose subjects match the stream’s subject configuration, subject to its retention and storage limits. Stream definition is an application contract: choose subjects, storage type, retention policy, maximum age/bytes/messages, and replica count from the test scenario.
The following command is illustrative of the official NATS CLI workflow; check the prompts and flags for the installed CLI release:
nats stream add ORDERS \
--subjects 'orders.>' \
--storage file \
--retention limits \
--replicas 1
The one replica setting is appropriate only for a single local server experiment. It is not a resilience configuration. A local stream with file storage demonstrates that JetStream can persist data in the server’s local store; it does not protect that store from WSL virtual disk loss, host failure, or accidental distribution deletion.
Publish a message on a subject included by the stream and inspect the stream state using the CLI version’s documented commands:
nats pub orders.created '{"order_id":"dev-1"}'
nats stream info ORDERS
If the publish does not appear in the stream, check server JetStream status, stream subject matching, account limits, and whether the CLI connected to the expected server. A successful Core NATS publish response is not a substitute for a JetStream publish acknowledgement.
Design consumer acknowledgement and replay
JetStream consumers track delivery state and can deliver messages according to configured delivery and acknowledgement policies. A durable consumer retains its state across client disconnects while the server and stored stream remain available. Acknowledgement should happen after the application has completed the side effect that makes the message safe to retire. A crash between the side effect and ack can cause redelivery, so processing should be idempotent.
Treat redelivery as an expected operational case, not a test anomaly. Configure bounded delivery attempts and a work queue or interest retention policy only when it matches the application contract. For event history, limits-based retention is often easier to reason about in a local exercise because messages can remain available for independent consumers until configured limits remove them. Retention policy names and CLI syntax are version-sensitive; verify them against the current official docs and CLI help before automating.
JetStream supports explicit message acknowledgements and publish acknowledgements, but neither one provides transactional coupling to an unrelated database. If a consumer writes a row and then acknowledges a message, a crash between those operations may produce duplicate work or an incomplete state transition. Test retries, duplicate suppression, and replay from a known sequence with the actual application store.
Core NATS remains useful for ephemeral request/reply or live notification paths where disconnected subscribers are allowed to miss messages. Do not switch all subjects to JetStream merely to say the system is durable. Decide per message class whether the contract requires a stream, replay, retention, and acknowledgement, then test that contract.
Inspect server and consumer state
Use the NATS CLI to inspect server, stream, and consumer state before diagnosing an application. Useful checks include:
nats server check jetstream
nats stream ls
nats stream info ORDERS
nats consumer ls ORDERS
The CLI command surface can change between releases, so keep a checked version and consult nats –help when a subcommand differs. Capture the server version, CLI version, stream configuration, consumer durable name, pending count, redelivery count, and last acknowledged sequence in test output.
A consumer that receives nothing may have a subject mismatch, an incorrect durable identity, a start policy that does not include the test message, or no JetStream connection at all. A consumer that repeatedly processes the same message may be failing to ack, crashing before ack, or using an ack wait shorter than its processing time. Read consumer state before deleting and recreating it; removing state can destroy useful evidence about the bug.
Stream subjects are filters, not routing transformations. A subject pattern should be narrow enough to keep unrelated application messages out of the stream. If a wildcard is used, test both the intended child subject and a nearby subject that must not be captured. This verifies that the stream configuration is an intentional part of the application contract rather than a broad catch-all left over from experimentation.
Limits-based retention is still bounded retention. Configure maximum age, bytes, and message count to match the test’s replay window, and verify what happens when each limit is reached. A replay test that only reads recently published messages does not prove the application can recover an old event after the stream has discarded it. For an important event archive, define an independent archival or backup destination.
Consumer delivery policy also matters to test repeatability. A new consumer that starts at the latest message will not replay older test events; a durable consumer may retain progress between test runs. Give each run an explicit consumer identity and starting policy, and clean only its own state. Do not delete the entire stream to make a test pass if that stream is shared with another local process.
Keep WSL persistence and service uptime distinct
JetStream’s file store is local to the server’s configured storage directory. It persists server data across a graceful process restart when the store remains available, but it is not an off-machine backup. WSL idle shutdown, Windows restart, host disk failure, package upgrade, or removing the distro can interrupt or remove the local server and its store.
If the data is disposable, define a cleanup procedure that targets only the test stream and test directory. If stream state matters, stop the NATS service cleanly, use a documented backup approach, preserve a copy outside the distro disk, and verify restore into a separate test server. Do not copy live JetStream store files while the server is mutating them and assume the resulting copy is consistent.
An enabled systemd unit controls service start within a running distro; it does not force WSL to stay awake. Applications should use a bounded connection/reconnect strategy and expose a clear dependency-unavailable state rather than hanging indefinitely when the local distribution has stopped.
Acceptance criteria for the local lab
Accept the setup when one known NATS server starts with JetStream enabled, the CLI connects to the intended endpoint, a stream captures only the intended subject pattern, a publish acknowledgement is observed, and a durable consumer can deliver, ack, and resume according to its configured policy. Intentionally interrupt a consumer before acknowledgement and verify how the application handles a redelivery.
Document which subjects are Core-only and which are persisted, the stream retention and limits, the storage path, and what happens when WSL stops. A local one-replica stream helps develop client logic; it does not demonstrate multi-node replication, fault tolerance, or end-to-end exactly-once business processing. Use an appropriately clustered environment for those tests.
Related:
- Apache Kafka in WSL: KRaft Local Brokers, Storage, and Client Paths
- systemd Journal Retention in WSL: Persistent Logs Without False Durability
Sources: