John Weldon

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:

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:

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:


  1. 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. ↩︎