Super-clusters
Acme's east cluster is healthy: three servers form a full mesh of
routes, with the ORDERS stream replicated across them. But half of Acme's customers are a continent away from east, and every
order they place has to cross the continent to reach it. So Acme brings up
a second region — a west data center near those customers, where their
orders land with far less latency.
Now two regions have to behave as one system — a publisher in either reaches a subscriber in the other — without dragging every message across the slow link between them.
You could run west as a completely separate NATS deployment. But then
a publisher in west could never reach a subscriber in east, and the
ORDERS stream in one region would be invisible to the other, leaving
you with two isolated islands.
You could also stretch one cluster across both regions, putting all six servers in a single mesh of routes. That works, but every server holds a route to every other server, and route traffic flows freely across the slow, expensive link between regions. A full mesh assumes the members are close together.
A super-cluster is the shape for this. It joins two independent
clusters into one logical system without combining them into a single
mesh. This page wires east and west together and shows how traffic
stays in its home region by default.
Two concepts on this page
This page introduces two ideas:
- Gateways: the cluster-to-cluster connection that joins clusters into a super-cluster.
- Geo-affinity: the behavior that keeps queue-group and request traffic in its home region, crossing a gateway only when no local worker can serve it.
How a gateway joins clusters
Inside a cluster, every server holds a route to every other server, the full mesh from the previous page. A gateway is a different kind of connection. It joins one cluster to another cluster.
A route ties two servers together and assumes they're close; a gateway ties two clusters together and assumes they're far apart. Stretch one cluster across both regions and every server holds a route to every server in the other region — a full mesh running over the slow link. A super-cluster keeps each region's mesh local and connects the regions through gateways instead, so the servers never mesh across the WAN, however many each region runs. Each server opens just one gateway connection to each other cluster, never one to every remote server.
A gateway also carries less. A route propagates interest freely inside its cluster; a gateway forwards a message to another cluster only when that cluster has a subscriber for it, and prefers a local worker over a remote one. Those two behaviors — interest-based forwarding and geo-affinity — get their own sections below, and they're what keep the inter-region link quiet.
A super-cluster (sometimes called a cluster of clusters, the one time this guide will write that phrase) is just clusters joined this way. Each cluster keeps running on its own. The gateway is the connection between them.
Gateways carry only what has interest
A gateway doesn't blindly forward every message to the other side. It carries a message across only when the remote cluster has a subscriber interested in that subject.
This is the whole reason gateways are cheap to run across a slow link. A
publisher in east floods orders.created thousands of times a second.
If nobody in west subscribes to orders.created, not one of those
messages crosses the gateway, because there is no interest on the far
side to forward them to.
The wire-level detail of how gateways advertise and track this interest lives in Reference → Gateway protocol. All you need here is the behavior: no interest on the far side means no traffic across the gateway.
Wiring east to west
You configure a gateway with a gateway {} block. The block names the
local cluster and lists the remote clusters to reach.
Here's the gateway {} block for the east servers. It declares the
local gateway name east, the port this server listens on for inbound
gateway connections, and a gateways array pointing at west:
# east gateway block — the name and the gateways list are shared
# by n1-east, n2-east, n3-east; each server picks its own port.
gateway {
name: "east"
port: 7222
gateways: [
{ name: "west", urls: ["nats://127.0.0.1:7322", "nats://127.0.0.1:7323", "nats://127.0.0.1:7324"] }
]
}
Two things to read carefully.
The name field identifies the cluster, not the server. Every server
in east uses the identical gateway name east. It has to match the name
in this server's cluster {} block exactly — set them differently and the
server refuses to start (cluster name conflicts between cluster and gateway definitions). A server's own entry
in the gateways array is ignored automatically, so the name and the
gateways list are identical across all three east servers. Only the
port each one listens on differs.
The gateways array lists every remote cluster, each with its name
and the urls to reach its gateway listeners. Listing all three west
URLs gives the connection somewhere to land if one west server is
down. That's also the hint that each remote server runs its own
gateway listener on its own port. The port line above is per server:
in a real deployment n1-east, n2-east, and n3-east each bind a
distinct gateway port, and the three west URLs point at three distinct
west listeners. The shared part is the name and the gateways list,
not the port.
You don't have to list every cluster exhaustively, though. Like routes,
gateways gossip: point a server at a few remote gateways and it
discovers the rest, including clusters it was never told about directly.
Seed east with west and west with a third region, and east picks up
that region on its own. The URLs are a starting point for discovery and a
fallback if one is down, not a list you must keep complete.
The west servers get the mirror-image block, with local name west
pointing back at the east gateway URLs:
# west gateway block — name and gateways list shared by
# n1-west, n2-west, n3-west; each server picks its own port.
gateway {
name: "west"
port: 7322
gateways: [
{ name: "east", urls: ["nats://127.0.0.1:7222", "nats://127.0.0.1:7223", "nats://127.0.0.1:7224"] }
]
}
Each cluster keeps the cluster {} block and the client port it had
before. The gateway {} block is additive. You're not rebuilding
east; you're adding a gateway connection from it to west.
To stand the whole thing up on your own machine without hand-editing six
files, this script writes every config and starts all six servers. East
uses client ports 4222-4224 and west 4225-4227; each server's gateway
name matches its cluster name, as above.
# Write a cluster+gateway config for each server, then start the super-cluster.
write_conf() { # name cluster client cluster-port route gateway-port remote remote-urls
cat > "$1.conf" <<EOF
server_name: $1
listen: 127.0.0.1:$3
cluster {
name: $2
listen: 127.0.0.1:$4
routes: [ nats://127.0.0.1:$5 ]
}
gateway {
name: $2
listen: 127.0.0.1:$6
gateways: [ { name: $7, urls: [ $8 ] } ]
}
EOF
}
EAST_URLS='nats://127.0.0.1:7222, nats://127.0.0.1:7223, nats://127.0.0.1:7224'
WEST_URLS='nats://127.0.0.1:7322, nats://127.0.0.1:7323, nats://127.0.0.1:7324'
write_conf n1-east east 4222 6222 6223 7222 west "$WEST_URLS"
write_conf n2-east east 4223 6223 6222 7223 west "$WEST_URLS"
write_conf n3-east east 4224 6224 6222 7224 west "$WEST_URLS"
write_conf n1-west west 4225 6225 6226 7322 east "$EAST_URLS"
write_conf n2-west west 4226 6226 6225 7323 east "$EAST_URLS"
write_conf n3-west west 4227 6227 6225 7324 east "$EAST_URLS"
for s in n1-east n2-east n3-east n1-west n2-west n3-west; do
nats-server -c "$s.conf" &
done
Stop everything later with pkill nats-server. These configs leave
JetStream off to keep the gateway demo simple — add the jetstream block
from the previous page if you
want ORDERS in the mix too.
Confirm the super-cluster formed
A super-cluster is only real if a message crosses it. Subscribe in west,
publish in east, and watch the order arrive on the far side of the
gateway.
Subscribe to orders.> on a west server, in its own terminal:
nats sub "orders.>" --server nats://localhost:4225
In another terminal, publish to an east server:
nats pub orders.created "order ord_8w2k" --server nats://localhost:4222
The west subscriber prints the order:
[#1] Received on "orders.created"
order ord_8w2k
It was published in east, crossed the gateway, and arrived in west.
That only happens once the gateway is live and west has advertised
interest in orders.> across it — the interest-based forwarding from
earlier. If nothing arrives, the
gateway never formed: check that each server's gateway name matches its
cluster name.
Geo-affinity keeps traffic local
Now the second concept. Acme runs the same fleet of order workers in
both regions: a queue group named order-workers, where each
message goes to exactly one worker in the group. (If queue groups are
hazy, the queue-groups primer is the
five-minute recap.)
Here's the question a super-cluster has to answer: when a worker exists
in both east and west, and an order is published in east, which
worker handles it?
The answer is geo-affinity. NATS prefers a local queue subscriber
first. An order published in east goes to an east worker, even
though a west worker is also subscribed and willing. The message never
crosses the gateway, because it doesn't need to.
This keeps the slow inter-region link quiet. Day to day, east orders
are served in east and west orders in west, so each region's
traffic stays within that region.
The gateway carries the message only when the local side can't
serve. If every east worker is down, an order published in east has
no local subscriber, so geo-affinity falls through and the message
crosses the gateway to a remote cluster that has an interested worker.
When more than one remote cluster has a worker for the queue group, NATS
forwards across every gateway with interest. The queue-group rule still
applies on the far side, so the order reaches exactly one worker, and no
work is lost in the process.
You can watch this local-first behavior. Start a queue worker in each region, each in its own terminal:
# terminal 1 — a worker in east
nats sub orders.created --queue order-workers --server nats://localhost:4222
# terminal 2 — a worker in west
nats sub orders.created --queue order-workers --server nats://localhost:4225
Publish an order in east:
nats pub orders.created "order ord_8w2k" --server nats://localhost:4222
The east worker prints it and west stays silent — geo-affinity served
it locally. Stop the east worker (Ctrl+C) and publish again: now west
prints it. With no local worker left, the order fell through to the gateway
and crossed to west, and nothing on the publisher changed to make it
happen.
Pitfalls
A super-cluster has a few traps that all come from one habit: thinking about it like a bigger cluster, which it is not. The connection between regions is a gateway, and gateways behave differently from routes.
Reaching for routes to span regions. A route is the intra-cluster
link that builds the full mesh. It assumes its peers are close and floods
freely. Stretch one cluster {} across east and west and every
server holds a route to every other server, with cluster traffic crossing
the slow link constantly. Don't grow a cluster across regions. Join two
independent clusters with a gateway {} block instead, so only
interested traffic crosses the gateway.
Mismatched gateway names. Each entry in the gateways array must use
the remote cluster's exact name. Typo it — wset for west — and the
server keeps dialing a cluster that never answers to that name
(Failing connection to gateway "wset", remote gateway name is "west"),
and the gateway never forms. Nothing fails at startup, so confirm with the
cross-region publish from Confirm the super-cluster
formed: if the order never reaches the
other side, a name is wrong.
Chatty cross-region traffic without local workers. Geo-affinity only
keeps traffic home when a local queue subscriber exists to serve it.
Run all your order-workers in east and let west clients publish, and
every west order crosses the gateway to reach a worker. The slow link
carries your steady-state load, not just failover. Don't centralize
workers in one region and assume geo-affinity avoids the cross-region
cost. Place a
queue subscriber for each workload in every region that produces that
work, so each region serves its own orders and the gateway stays quiet.
Where you are
Acme's deployment grew from one cluster to a super-cluster:
eastandwestare independent clusters, each a full mesh of routes.- A gateway joins them, carrying only traffic with interest on the far side.
- Geo-affinity keeps queue-group and request traffic in its home region, crossing the gateway only when the local side can't serve.
- Nothing about publishing or subscribing changed — the same
orders.createdpublish still reaches an interested subscriber, now even across regions.
What's next
The next page pushes NATS past the data center entirely. A
leaf node is a server that dials out to
the east cluster — a single outbound connection, so it works from a
factory or a laptop with no inbound ports open. That one connection is
two-way, so once it's up the leaf bridges order traffic in both directions.
See also
- Reference → Gateway protocol — the wire-level detail of how gateways advertise interest and connect.
- Core Concepts → Queue Groups — the recap of the queue-group behavior geo-affinity steers.
- Operate → Clustering & Replication — how a stream's replicas work within one cluster (a gateway carries interest across regions, not stream copies).