Subject mapping
Every message so far has kept the subject its publisher gave it: a client
publishes orders.created, and the server routes it to whoever subscribed to
orders.created. Subject mapping breaks that one-to-one link. The server
rewrites a message's subject the moment it arrives, before it looks for
interested subscribers, and it does the rewrite from its own configuration —
the publisher sends the same command and never sees the change.
Acme has a use for that today. Its old mobile app still publishes new orders
to orders.placed, but every service moved to orders.created several pages
ago. Instead of shipping a new app build, Acme tells the server to map
orders.placed onto orders.created. The app keeps publishing
orders.placed; the packers and analytics keep subscribing to
orders.created; the server joins the two.
This is the account-level subject mapping that the JetStream chapter points back to. It's different from a stream's subject transform or its republish setting: those rewrite subjects as a message enters or leaves a JetStream stream, and you configure them on the stream. Server-side mapping runs earlier, on plain core subjects, before any stream is involved. For the stream side, see Subject mapping and transforms; this page stays on the core, server-config side.
Rename a subject
A subject mapping is a rule in server configuration that rewrites one subject to another. You give the server a source subject and a destination subject; when a message arrives on the source, the server delivers it as if the publisher had named the destination.
Mapping lives in the server's configuration file, so this is the first page in
the chapter that needs one. Earlier pages started the server with a bare
nats-server; from here you write a small config file and start the server
with nats-server -c server.conf. A config file that renames Acme's legacy
subject looks like this:
# server.conf
mappings {
orders.placed: orders.created
}
nats-server -c server.conf
Now a publisher to orders.placed reaches every subscriber on
orders.created. The publisher's command doesn't change, and it gets no signal
that a mapping exists — the rewrite happens on the server, on the way in.
The top-level mappings block belongs to the server's built-in account, so
on a plain server with no accounts or authentication it covers every client.
Mapping is always scoped to an account: with several accounts configured, each
has its own mappings and they never cross the account boundary.
The mappings block is reloadable. Edit server.conf and run
nats-server --signal reload to apply the change without restarting; the
signal tells the running server to re-read its config, and open connections
stay up.
Preview a mapping before you apply it
You don't have to change a running server to see what a mapping does.
nats server mappings takes a source, a destination, and a subject, runs the
same transform code the server uses, and prints the result — with no server and
no connection.
- CLI
#!/bin/bash
# Check a mapping without a running server. `nats server mappings` takes a
# source pattern, a destination pattern, and a subject, runs the same
# transform code the server uses, and prints the result. It opens no
# connection, so you can try a mapping before you ever put it in config.
# A literal rename: orders.placed always maps to orders.created.
nats server mappings "orders.placed" "orders.created" orders.placed
# A token reference: {{wildcard(1)}} pulls the token the first * matched into
# the destination, so orders.legacy.us becomes orders.us.created.
nats server mappings "orders.legacy.*" "orders.{{wildcard(1)}}.created" orders.legacy.us
orders.created
orders.us.created
The second command shows a token reference: {{wildcard(1)}} in the
destination stands for the token the first single-token wildcard matched in the
source. Source orders.legacy.* matched us, so the destination
orders.{{wildcard(1)}}.created filled in as orders.us.created. You number
wildcards left to right, so {{wildcard(2)}} would be the second *.
Split traffic by weight
A mapping can send a fraction of matched messages to a different subject. A weighted mapping lists several destinations, each with a weight from 0 to 100, and the server picks one per message at random in proportion to the weights.
Acme is testing a new packer build. It wants a tenth of new orders handled by
the canary and the rest handled normally, so it maps orders.created and gives
the canary a weight of 10:
# server.conf
mappings {
orders.created: [
{ destination: orders.created.canary, weight: 10 }
]
}
The weights you list add up to 10, not 100. The server sends the remaining 90
out of every 100 messages to the source subject unchanged, so the packers
already on orders.created keep handling most orders while the canary on
orders.created.canary samples the rest. Whenever your listed weights total
less than 100, the leftover share stays on the original subject.
To split between two new subjects instead, list both and make the weights total 100 — 50 and 50, say, for an even A/B test where nothing stays on the original. If you want the leftover share dropped rather than kept — to test how a subscriber copes with loss — list the source subject itself as a destination, which tells the server your weights are final and stops it topping them up. This works because the source here is a literal subject.
The split is per-message and random, the same way a queue group picks a member: over a few messages it can look lopsided, and over thousands it converges on the weights you set. Each weight must be 100 or less, and the weights for one source must total 100 or less, or the server rejects the config.
Partition by a token
A weighted mapping spreads messages at random across its destinations.
Partitioning spreads them deterministically instead: it routes each message
by hashing one of its tokens into a fixed bucket, so the same token value always
lands in the same bucket. In a destination, {{partition(n, 1)}} stands for
that bucket — it hashes the token the first single-token wildcard matched into
one of n buckets, numbered 0 to n-1.
Acme wants each order handled by one fixed packer pool, so everything about an
order stays together: one pool's cache, no coordination between pools. The order
id decides the pool, which means the id has to be in the subject. So the app
publishes new orders with the id as the last token, like
orders.created.ord_8w2k, and Acme splits them into three buckets by hashing
that id:
# server.conf
mappings {
"orders.created.*": "orders.created.{{partition(3, 1)}}.{{wildcard(1)}}"
}
The source orders.created.* matches any order id in the last token. The
destination builds a new subject: {{partition(3, 1)}} becomes the bucket for
that id, and {{wildcard(1)}} carries the id itself through. So
orders.created.ord_8w2k becomes orders.created.0.ord_8w2k.
The bucket is deterministic, so nats server mappings tells you exactly where
an id lands before you commit the config:
nats server mappings "orders.created.*" "orders.created.{{partition(3, 1)}}.{{wildcard(1)}}" orders.created.ord_8w2k
orders.created.0.ord_8w2k
Change the id and the bucket changes with it: ord_7mn3 maps to
orders.created.1.ord_7mn3, and ord_2zr9 to orders.created.2.ord_2zr9.
Feed the same id twice and you get the same bucket both times — that's what
makes it safe to route on.
Each pool subscribes to its own bucket. The packers for bucket 0 subscribe to
orders.created.0.* under a shared queue group; buckets 1 and 2 have their own
pools on orders.created.1.* and orders.created.2.*.
Queue groups share the work inside a bucket;
the partition decides which bucket. Because the mapping rewrites the subject
before the server routes it, the pools subscribe to the bucket subjects, not to
orders.created.* — a subscriber still on the pre-map orders.created.ord_8w2k
receives nothing, because the server moved the message to a bucket subject
before matching interest.
partition and wildcard cover almost every core mapping. The transform
language has more functions — split, slicefromleft, and others reshape a
single token — and streams use the same set, so the full list lives with
the transform language.
Cluster-scoped destinations
A destination can carry a cluster field. The mapping then applies only to
messages published through a server in that named cluster, and a server falls
back to the unscoped mapping when none matches its cluster. That lets one source
map differently in different regions, so it only matters once you run more than
one cluster — see Super-clusters. A single
server has one scope and doesn't need it.
Try it
Put the partition mapping on a real server and watch orders fall into buckets. Save the config and start the server with it:
# server.conf
mappings {
"orders.created.*": "orders.created.{{partition(3, 1)}}.{{wildcard(1)}}"
}
nats-server -c server.conf
Then open one subscriber per bucket and publish three orders:
- CLI
#!/bin/bash
# Deterministic partitioning across three packer pools.
#
# The server (server.conf) maps:
# orders.created.* -> orders.created.{{partition(3, 1)}}.{{wildcard(1)}}
# so every order id hashes to a fixed bucket: 0, 1, or 2. The same id always
# lands in the same bucket.
#
# Open one subscriber per bucket, each in its own terminal. A real pool would
# add --queue to share a bucket across several packers; one subscriber per
# bucket is enough to see the split.
# Terminal 1 — bucket 0
nats sub "orders.created.0.*"
# Terminal 2 — bucket 1
nats sub "orders.created.1.*"
# Terminal 3 — bucket 2
nats sub "orders.created.2.*"
# Terminal 4 — publish three orders with the id as the last token.
# ord_8w2k hashes to bucket 0, ord_7mn3 to bucket 1, ord_2zr9 to bucket 2.
nats pub orders.created.ord_8w2k '{"order_id":"ord_8w2k","customer":"acme-co","total_cents":4200,"ts":"2026-05-22T10:14:22Z"}'
nats pub orders.created.ord_7mn3 '{"order_id":"ord_7mn3","customer":"globex","total_cents":7800,"ts":"2026-05-22T10:14:25Z"}'
nats pub orders.created.ord_2zr9 '{"order_id":"ord_2zr9","customer":"initech","total_cents":1500,"ts":"2026-05-22T10:14:29Z"}'
Each order arrives at exactly one bucket: ord_8w2k at bucket 0, ord_7mn3 at
bucket 1, ord_2zr9 at bucket 2. Publish orders.created.ord_8w2k again and it
returns to bucket 0 — the hash doesn't drift.
Pitfalls
A mapping quietly changes who receives what. The server rewrites the subject
before it checks interest, so a mapping can pull messages away from subscribers
still listening on the original subject. Map orders.created entirely onto a
new subject and the packers on orders.created go silent — every order lands
somewhere else, with no error and no warning. Before you map a subject that
services already use, check what subscribes to it, and keep a share on the
original (the weighted remainder is one way) if those subscribers still need it.
Weights under 100 keep the rest, they don't drop it. List one destination at
weight 10 and it's easy to assume the other 90 percent disappears. It doesn't:
the server routes that 90 percent to the source subject. If you really want the
remainder dropped — say, to test loss — list the source subject itself as a
destination so the server treats your weights as final. This only works for a
literal source like orders.created.
A partition count is part of the subject contract. Which bucket an order
lands in depends on n in partition(n, ...). Raise partition(3, ...) to
partition(4, ...) later and the same order id can hash to a different bucket,
so a pool's subject filter silently starts covering a different set of orders.
Pick the bucket count once, up front, the way you would for any sharded system.
Where you are
Your running session picked up its first configuration file:
- The one local
nats-servernow starts withnats-server -c server.confand reloads withnats-server --signal reload. - A mapping bridges the legacy
orders.placedpublisher ontoorders.created, with nothing changed in the app. - The server can also split a subject's traffic by weight, or shard it into fixed buckets by a hashed token, all before any subscriber is chosen.
The subject a publisher writes is no longer always the subject a subscriber matches — the server config sits in between.
What's next
Every publish and subscribe so far assumed the connection to the server was just there. The next page, Connection lifecycle, looks at that connection directly: what happens to your client and its in-flight messages when the link drops, and how to watch it reconnect.
See also
- Subject mapping and transforms — the stream side: transforms that rewrite subjects into a stream, and republish that emits them back out.
- Reference → mappings — the config keys
destination,weight, andcluster, and their exact rules. - Queue groups — the pools that share the work inside each partition bucket.
- Super-clusters — cluster-scoped destinations across regions.