Leaf nodes
Acme now runs a super-cluster: east and west, joined by gateways.
Both clusters live in cloud regions Acme controls. The ORDERS workload
moves freely between them.
The next site is different. Acme has a factory floor that publishes
orders.* from machines on the plant network. That network sits behind
a firewall. Nothing on the internet can dial into it. The factory can
only reach out.
A gateway won't help here. A gateway joins two clusters that can both accept connections from each other. The factory can't accept anything. This page introduces one shape built for exactly that constraint: the leaf node. Everything else on the page (how it bridges interest, how it binds to an account) is part of understanding that one shape.
What a leaf node is
A leaf node is a NATS server that opens an outbound connection to a remote NATS system and bridges subject interest across it.
The outbound direction is what makes this work. The factory's server dials
the east cluster; east never dials the factory. As long as the
factory can make one connection out to the hub, the bridge works: no
inbound firewall rule, no public address on the factory side.
The server the leaf dials is the hub. In Acme's case the hub is the
east cluster. The leaf is factory-1.
Once the link is up, the leaf is a regular NATS server to anyone on the
factory floor. A machine publishes orders.created to factory-1 the
same way it would publish to any server. It doesn't know or care that
factory-1 is a leaf.
What the leaf adds, behind that ordinary front, is the bridge across
the link. When a client somewhere on the hub subscribes to
orders.>, that interest flows down the leaf link, and factory-1
forwards matching messages up to the hub. When a factory machine
subscribes, hub traffic flows down. The leaf carries only subjects that
have interest on the other side. This is the same interest propagation
clusters use internally; the leaf just extends it across one outbound
hop. (Routing and replication inside a cluster are covered in the
Clustering deep dive.)
The wire-level detail of the leaf node protocol (how interest and messages are framed across the link) is documented in Reference → Leafnode protocol. Here we only need the config and the bridging behavior.
The hub side: accept leaf connections
The hub opens a port for leaf nodes to dial. On every east server,
add a leafnodes {} block with a listen address.
The default leaf node port is 7422. It's a fourth kind of listener, separate from the client port (4222), the route port (6222), and the gateway port (7222) — one port per kind of connection.
Here's n1-east, the same east cluster config from before, now also
accepting leaf connections. Only the leafnodes block is new:
# n1-east.conf — the east cluster config, now accepting leaf connections
server_name: n1-east
listen: 127.0.0.1:4222
cluster {
name: east
listen: 127.0.0.1:6222
routes: [
nats://127.0.0.1:6223
]
}
# NEW: accept inbound leaf node connections on 7422
leafnodes {
listen: 127.0.0.1:7422
}
Add a leafnodes {} block to n2-east and n3-east too. Since all three
run on one machine, give each its own leafnode port — 7423 and 7424, the
way their client and cluster ports are already offset. A leaf can dial
any hub server, so listing several gives it somewhere to reconnect if one
is down.
The leaf side: dial out with a remote
The leaf does the opposite. Instead of listening for leaf
connections, factory-1 declares a remote: the hub it dials.
A remote lives in leafnodes.remotes. The one field it can't do without
is urls — where to dial the hub:
# factory-1.conf — a leaf that dials the east cluster
server_name: factory-1
listen: 127.0.0.1:4300
leafnodes {
remotes: [
{
urls: [
"nats://127.0.0.1:7422"
"nats://127.0.0.1:7423"
"nats://127.0.0.1:7424"
]
}
]
}
factory-1 has no cluster {} block and no gateway {} block. It's a
standalone server that reaches the rest of Acme through one outbound
link — that's what lets it run on the plant network with nothing but
egress. Its own client port (4300) is separate from the hub's.
The urls point at each hub server's leafnode listen port. Listing all
three gives the leaf somewhere to reconnect if one hub server is down; it
tries them in turn.
One leaf isn't limited to one hub. remotes is a list: a leaf can hold
several, each dialing a different NATS system, and bridge them all through
the one server. A leaf can also run its own leafnodes { listen } block and
become a hub for leaves further out — leaf links compose into trees, not
just a single hub and spoke.
In production a remote usually carries two more fields — credentials to
prove who the leaf is, and account to bind its traffic to a specific
account. We skip both here — the leaf bridges in the default account —
and come back to them in
Accounts: the production layer.
Accounts: the production layer
The auth-free setup above bridges in NATS's default account, which is all the demo needs. A production deployment usually isn't auth-free: it puts each workload in its own account — NATS's unit of subject isolation, a flat space of subjects separate from every other — and a leaf remote carries two more fields to join one:
accountselects which local account on the leaf the bridged interest joins.credentialsis a.credsfile that proves the leaf's identity to the hub, which attaches it to the matching account there.
Point both ends at the same account and the factory floor and the cloud share one isolated subject space across the link. Setting up accounts, minting those credentials, and authorizing leaf connections is the job of the Security deep dive; here we stay in the default account.
Local clients stay hidden behind the leaf
A factory machine connects to factory-1 as a plain client. It never
appears on the hub as a connection. The hub sees one thing: the leaf
link from factory-1.
This is the address-space property of a leaf. The leaf's local clients live behind it. The hub deals with the leaf, not with the hundred machines on the plant network. Add a thousand more machines and the hub still sees one leaf link.
The bridge is by interest, not by exposing clients. A hub subscriber
to orders.> receives factory orders without ever knowing how many
machines produced them, or that they came from a leaf at all.
That covers connections. Subjects are separate: whether a factory subject stays on the floor or reaches the hub depends on the account the leaf binds to. Bound to its own account, only the subjects that account imports and exports cross the link — that's address-space isolation, an account decision (the production layer above), not something the leaf link gives you for free. In the default account this page uses there's no subject boundary; interest flows across the leaf the way it flows across a cluster's routes. Connections hide on their own; subjects need an account.
Send a message across the leaf link
Stand up the hub and the leaf, subscribe on the factory floor, and publish from the hub. The order crosses the leaf link in the hub-to-leaf direction.
Subscribe to orders.> on factory-1, in its own terminal:
nats sub "orders.>" --server nats://localhost:4300
In another terminal, publish to an east hub server:
nats pub orders.shipped "order ord_8w2k shipped" --server nats://localhost:4222
The order arrives on the factory floor:
[#1] Received on "orders.shipped"
order ord_8w2k shipped
The leaf carried the factory's orders.> interest up to the hub, and the
hub forwarded the matching message down — neither side opened a connection
to the other beyond the single leaf link.
JetStream over a leaf
If factory-1 runs its own JetStream (a local ORDERS store on the
plant floor), it needs a JetStream domain.
A domain is a name that isolates one JetStream system from another across a leaf link, so the factory's streams and the hub's streams stay separate and addressable. Without distinct domains, a leaf's JetStream and its hub's JetStream collide.
Copying stream data across the leaf (a local factory mirror of the hub's
ORDERS, or sourcing factory orders up into a hub stream) is the
subject of JetStream → Mirrors and sources.
We only name the domain concept here.
Pitfalls
Two problems commonly come up the first time you attach a leaf.
Treating the leaf like an inbound connection. A leaf dials out.
The hub listens on 7422; the leaf declares a remotes entry pointing
at the hub. People reverse this (a listen on the factory, expecting
the hub to dial in) and nothing connects, because the hub never reaches
out. Put the remotes block on the side that has only
egress (the factory), and the leafnodes { listen } block on the side
that accepts connections (the hub).
Expecting JetStream to span the leaf without a domain. A leaf and
its hub don't automatically get separate JetStream systems. If
factory-1 enables JetStream while sharing the hub's system account, it
extends the hub's JetStream rather than running its own, so a stream
you create on the factory floor may land on the hub, not locally. Give
the leaf its own JetStream domain when you want a distinct local
store. Copying data between the two domains across the leaf is
JetStream → Mirrors and sources.
Where you are
Acme's deployment now reaches the edge:
eastandwestclusters, joined by gateways (a super-cluster)- a leaf node,
factory-1, dialingeastover an outbound link on port 7422 - the leaf bridging
orders.*interest both ways over that one link, in the default account (bind it to a named account in production) - factory machines connected to
factory-1as plain clients, hidden behind the leaf, sharing one subject space with the cloud
Publishing and subscribing didn't change. A factory machine publishes
orders.created to factory-1 exactly the way a client did against the
dev server n1 on Single server.
What's next
You've met all four shapes: a single server, a cluster, a super-cluster, and a leaf. The next page, Putting it together, composes them into the full Acme picture and draws the address-space isolation that makes the whole thing scale.
See also
- Reference → Leafnode protocol — the wire-level framing of the leaf link
- Security deep dive — minting leaf credentials and authorizing leaf connections on the hub
- JetStream → Mirrors and sources — copying stream data across a leaf with JetStream domains