Per-Region Service Routing with NATS Subject Mappings
When the same application runs in multiple regions, where does a request go? You can encode the region in the subject (payments.region1.process) and make every app region-aware. You can stand up a routing proxy and accept the extra hop. Or you can let the messaging fabric handle it: applications publish to a generic subject, and infrastructure decides per region where the request lands.
NATS subject mappings, scoped per cluster within a super-cluster, do this cleanly. This post walks through the topology requirement, the configuration shape, how failover works, and the simpler alternative you should consider first.
Topology Matters: Cluster vs Super-Cluster
The pattern depends on a topology distinction that’s easy to get wrong:
- A NATS cluster is a full-mesh of servers connected by routes. Account configuration is synchronized across the cluster – every server agrees on the account’s permissions, imports, exports, and subject mappings. Servers in a single cluster cannot carry divergent mapping rules; if you try, it’s a misconfiguration.
- A super-cluster is multiple clusters connected by gateway links. Each cluster has its own server configs. Across the super-cluster, the same account can carry different mapping rules in each cluster, because each cluster’s mappings come from local config.
The per-region routing pattern requires a super-cluster. If you have a single cluster spanning regions (“stretch cluster”), every server applies the same mappings and you can’t make payments.process mean different things in different places.1
The Pattern
Two clusters in a super-cluster, each running a regional copy of a payment-processing service. Applications publish to payments.process regardless of where they’re deployed. The local server maps that subject to the regional service’s subject before the message leaves the publisher’s server.
Region 1 cluster config:
accounts {
PAYMENTS {
mappings = {
"payments.process": [
{dest: "services.region1.payments", weight: 100%}
]
}
}
}
Region 2 cluster config:
accounts {
PAYMENTS {
mappings = {
"payments.process": [
{dest: "services.region2.payments", weight: 100%}
]
}
}
}
Each region’s payment service subscribes to its regional subject (services.region1.payments or services.region2.payments). Applications never know which one they’re hitting – they publish to payments.process and the local server rewrites the destination.
How the Mapping Resolves
The mapping is applied at the server where the publish originates, before any gateway hop. A client connected to Region 1’s cluster publishing payments.process has the subject rewritten to services.region1.payments at the local server; the message routes from there. Region 2’s mapping does not re-apply in transit – mappings are not chained across gateways.
This is what makes per-cluster mappings work: the application publishes a generic subject, and each cluster independently decides what that subject means for its locally-connected clients.
Failover
For active/standby routing across regions, change the mapping destination and reload. Suppose Region 1 is the active payment processor and Region 2 is on standby. Region 1’s apps already map locally as above. To fail Region 1’s traffic over to Region 2, edit Region 1’s config so that payments.process maps to a subject that crosses the gateway:
accounts {
PAYMENTS {
mappings = {
"payments.process": [
{dest: "services.region2.payments", weight: 100%}
]
}
}
}
Then reload:
# If pid_file is set in server.conf:
nats-server --signal reload
# Otherwise, supply the PID explicitly:
nats-server --signal reload=<pid>
# or: kill -HUP $(pgrep -x nats-server)
No application restarts. No client reconnections. The next request published in Region 1 routes across the gateway to Region 2’s service. Failing back is the same operation in reverse.
There is a short cutover window: requests already in flight when the reload signal is processed may be delivered to the old destination. For low-volume workloads this is negligible; for high-throughput services, expect a brief overlap and design retries accordingly.
The Simpler Alternative
Before reaching for subject mappings, consider whether a plain service-import pattern is enough. NATS service imports allow the same subject to be imported from different source accounts – a single account can import services.region1.payments from one source account and services.region2.payments from another, and the caller picks which one to invoke.
This works when the application or an intermediary is willing to select between two named subjects. It does not require a super-cluster, it does not require config reloads to fail over (the application or a service mesh can pick), and the routing logic is visible in the caller’s code rather than in cluster configuration.
The subject-mapping pattern is the better fit when:
- Applications should be truly region-unaware – no awareness of two regional endpoints, no selection logic
- You want routing decisions to live in infrastructure configuration, not in application code
- Your topology already is a super-cluster
- You want failover triggered by an operator config change, not by application logic or a service-mesh policy
If you have a single cluster spanning regions, neither approach gives you per-region mapping within an account. Stretch-cluster topology limits you to homogeneous routing; reach for a super-cluster first.
Operational Caveats
A few things worth knowing before relying on this pattern in production:
- Reload requires a PID source.
nats-server --signal reloadneeds a pidfile (setpid_filein the server config) or an explicit PID argument. Without either, the signal has nowhere to land. - Failover is operator-triggered. This pattern gives you application transparency, not automatic failover. The cutover happens when an operator edits config and sends a reload – there is no built-in health check or auto-switch.
- Mappings are not stream replication. Subject mappings rewrite the destination of an individual message at publish time. They are not a substitute for JetStream cross-domain sourcing if you need persistent stream replication across regions.
- Within-cluster account configs must agree. If two servers in the same cluster carry different mappings for an account, the cluster is misconfigured. The per-cluster scoping that makes this pattern work is a super-cluster property; do not try to leverage it within one cluster.
-
Subject Mapping and Partitioning – Mappings are defined per account in the server config. Account configuration is synchronized within a cluster but not across clusters in a super-cluster, which is what enables the per-region pattern. Weighted mappings extend the same configuration shape to split traffic by percentage – useful for canary deployments inside a single region, but a separate dimension from per-region routing. ↩︎