A community member asked whether it makes sense to migrate from Core NATS to JetStream when an edge deployment can have anywhere from one to dozens of unstable nodes, with no central node that can be assumed to stay online.
The short answer: do not model that environment as one fully dynamic JetStream cluster unless you can provide stable JetStream membership and quorum. JetStream can still be useful, but the topology usually needs to separate routing from persistence.
JetStream clustering uses RAFT for metadata and replicated stream state. RAFT is designed around a known group of peers that can form quorum. That has important consequences for highly dynamic edge environments:
This does not mean JetStream is unsuitable for edge computing in general. It means that one JetStream cluster spanning all unstable edge members is usually the wrong abstraction.
If you have a stable hub, regional site, or subset of reliable nodes, JetStream clustering may fit there. Less stable nodes can connect through leaf nodes, and data can be moved with JetStream sourcing or mirroring where appropriate. See the NATS documentation for:
But if there is no stable core at all, the design needs different tradeoffs.
One pattern that may work is to run two NATS server processes per physical or logical edge member:
Conceptually:
1edge member A2 gateway NATS process <---- Core routes ----> gateway NATS process3 local JetStream process -- leaf connection --> gateway NATS process4
5edge member B6 gateway NATS process <---- Core routes ----> gateway NATS process7 local JetStream process -- leaf connection --> gateway NATS processIn this model:
replicas: 1), which is the only option on a single-server JetStream domain.This provides local persistence and JetStream client semantics at each member, but it is not the same as a replicated, highly available JetStream cluster. If the local storage process or disk is lost, a replicas=1 stream does not have another JetStream replica to recover from.
The following is an illustrative shape, not a complete production configuration. Authentication, authorization, TLS, monitoring, storage paths, and operational controls need to be designed for your environment.
A Core gateway process might look like this:
1listen: 192.0.2.10:42222server_name: gateway-a3
4accounts: {5 $SYS: {6 users: [{ user: sys, password: pass }]7 }8 EDGE: {9 # No jetstream directive here: the gateway is a Core-only routing process.10 users: [11 { user: app-user, password: app-pass },12 { user: leaf-user, password: leaf-pass }13 ]14 }15}16
17cluster {18 name: edge-core19 listen: 0.0.0.0:424820
21 routes: [22 nats://192.0.2.10:4248,23 nats://192.0.2.11:424824 ]25}26
27leafnodes {28 port: 742229}A local standalone JetStream process on the same member might look like this:
1listen: 127.0.0.1:41112server_name: storage-a3
4accounts: {5 $SYS: {6 users: [{ user: sys, password: pass }]7 }8 EDGE: {9 jetstream: enable10 users: [{ user: app-user, password: app-pass }]11 }12}13
14# The jetstream block enables JetStream and sets a unique domain for this member.15jetstream {16 store_dir: /var/lib/nats/jetstream17 domain: storage-a18}19
20leafnodes {21 remotes: [22 {23 urls: ["nats://leaf-user:leaf-pass@192.0.2.10:7422"]24 account: EDGE25 }26 ]27}The important detail is the account field on the leaf node remote. The user in the URL determines which account the remote side authenticates into. The account field determines the local account used by the leaf connection.
Leaf node connections connect accounts one-to-one. The account names on each side do not have to match, but being explicit avoids surprising behavior.
If messages appear on the Core side but do not flow to the leaf side, check the account binding first.
For example, this is incomplete if you expect the local EDGE account to participate:
1leafnodes {2 remotes: [3 { urls: ["nats://leaf-user:leaf-pass@192.0.2.10:7422"] }4 ]5}Prefer this:
1leafnodes {2 remotes: [3 {4 urls: ["nats://leaf-user:leaf-pass@192.0.2.10:7422"]5 account: EDGE6 }7 ]8}Also remember:
$SYS account for application traffic.nats server report leafnodes and nats trace can help verify connectivity and subject interest.With standalone JetStream per member, each storage process has its own JetStream domain, and each domain name must be unique so a specific member can be addressed unambiguously. By default, a client’s JetStream context uses the JetStream available through the server it connects to: if it connects directly to a storage process, that is the local domain; if it connects to a Core gateway process, there is no local JetStream, so the client must name a domain explicitly.
A client can target a specific JetStream domain when it needs to interact with a remote member’s streams, assuming the topology and account permissions allow that access.
That means an offline member is not automatically limited to only what it saw while online. Depending on your design, a process can later read from another member’s JetStream domain. However, that is different from saying that all streams are automatically replicated everywhere.
The local-standalone pattern avoids a fragile global JetStream RAFT group, but it also means JetStream will not automatically replicate every member’s streams to every other member.
You have a few options, each with tradeoffs:
JetStream sourcing can copy messages from one stream into another. This can be useful when relationships between streams are known and managed.
However, sourcing is not a fully dynamic membership system. If nodes come and go arbitrarily, an administrative controller or operator process may need to add, remove, and reconcile source configuration.
Another option is an application-level replication agent. Such an agent can:
This gives you control, but it moves part of the distributed data problem into your software.
Be careful with stream subjects. If the same published subject is captured by streams in multiple domains, messages may be ingested in multiple places. That may be exactly what you want for status fanout, or it may create duplicates and reconciliation work.
For many edge systems, it is safer to design clear subject ownership, for example by including node identity or domain identity in the subject hierarchy.
If the environment cannot provide any stable JetStream placement, and if local persistence does not add enough value, Core NATS plus application-level delivery mechanisms may still be the more honest design.
Core NATS is lightweight and flexible, but it does not provide JetStream persistence, durable consumers, or replay by itself. If you need stronger delivery confidence on top of Core NATS, you will need to design those semantics explicitly in the application.
Also consider the cost of a full mesh. Even without JetStream RAFT, a topology where every server connects to every other server can become operationally difficult as node count and network instability increase. If you can introduce a hub-and-spoke model with stable hubs, the overall NATS design usually becomes simpler.
For highly unstable edge clusters with no stable core:
JetStream can still be valuable in this kind of architecture, but the value comes from carefully scoped persistence domains rather than pretending that an unstable full mesh can behave like a stable replicated storage cluster.
Want help from the NATS experts? Meet with our architects to get help tailored to your use case and environment.



News and content from across the community