Subjects
NATS implements a subject-based messaging system where publishers and subscribers communicate through named channels called subjects. This provides a location-transparent, interest-based communication pattern that automatically routes messages across distributed NATS servers.
What is a Subject?
A subject is a string of characters that forms a name which publishers and subscribers use to find each other. It acts as the address for message routing within NATS. Subjects are case-sensitive and can contain any UTF-8 characters except whitespace, tabs and line breaks. It's a good practice to use alphanumeric characters along with - (dash) and _ (underscore) for readability.
In the animation above, events.data is the subject - it's the named channel that connects the publisher to all subscribers without any direct addressing.
Subject Hierarchies
The . (dot) character creates a subject hierarchy, enabling logical grouping of related subjects. This hierarchical namespace helps organize your messaging architecture:
orders.retail.placed
orders.retail.shipped
orders.retail.returned
orders.wholesale.placed
orders.wholesale.shipped
orders.wholesale.returned
Wildcards
NATS provides two wildcards for flexible subscription patterns. While publishers always send to a fully specified subject, subscribers can use wildcards to receive messages from multiple subjects.
The subscriber with pattern orders.retail.* receives messages from matching subjects (green and blue paths) but not from non-matching subjects (red path). The * wildcard matches exactly one token.
Single Token Wildcard (*)
The * wildcard matches exactly one token. For example:
-
orders.retail.*matches:orders.retail.placedorders.retail.shippedorders.retail.returned
-
orders.*.placedmatches:orders.retail.placedorders.wholesale.placed
- CLI
- JavaScript/TypeScript
- Go
- Python
- Java
- Rust
- C#/.NET
# Subscribe using single token wildcard.
# Since each sub waits indefinitely, try each sub
# in a different terminal or just repeat the
# publishes for each sub.
nats sub "orders.*.shipped"
nats sub "orders.*.placed"
nats sub "orders.retail.*"
# Publish to specific subjects (use a different terminal)
nats pub orders.wholesale.placed "Order W73737"
nats pub orders.retail.placed "Order R65432"
nats pub orders.wholesale.shipped "Order W73001"
nats pub orders.retail.shipped "Order R65321"
async function process(subject: string) {
const sub = nc.subscribe(subject);
const label = `[${subject}]`.padEnd(20);
for await (const msg of sub) {
console.log(`${label}${msg.string()} (${msg.subject})`);
}
}
process("orders.*.shipped").catch(console.error);
process("orders.*.placed").catch(console.error);
process("orders.retail.*").catch(console.error);
// Publish to specific subjects
nc.publish("orders.wholesale.placed", "Order W73737");
nc.publish("orders.retail.placed", "Order R65432");
nc.publish("orders.wholesale.shipped", "Order W73001");
nc.publish("orders.retail.shipped", "Order R65321");
// Subscribe with single token wildcard
nc.Subscribe("orders.*.shipped", func(m *nats.Msg) {
fmt.Printf("[orders.*.shipped] %s (%s)\n", string(m.Data), m.Subject)
})
nc.Subscribe("orders.*.placed", func(m *nats.Msg) {
fmt.Printf("[orders.*.placed] %s (%s)\n", string(m.Data), m.Subject)
})
nc.Subscribe("orders.retail.*", func(m *nats.Msg) {
fmt.Printf("[orders.retail.*] %s (%s)\n", string(m.Data), m.Subject)
})
// Publish to specific subjects
nc.Publish("orders.wholesale.placed", []byte("Order W73737"))
nc.Publish("orders.retail.placed", []byte("Order R65432"))
nc.Publish("orders.wholesale.shipped", []byte("Order W73001"))
nc.Publish("orders.retail.shipped", []byte("Order R65321"))
# Subscribe to shipped orders
sub_shipped = await nc.subscribe("orders.*.shipped")
# Subscribe to placed orders
sub_placed = await nc.subscribe("orders.*.placed")
# Subscribe to retail orders
sub_retail = await nc.subscribe("orders.retail.*")
async def reader(sub, label):
async for msg in sub:
print(f"[{label:<18}] {msg.data.decode()} ({msg.subject})")
asyncio.create_task(reader(sub_shipped, "orders.*.shipped"))
asyncio.create_task(reader(sub_placed, "orders.*.placed"))
asyncio.create_task(reader(sub_retail, "orders.retail.*"))
await nc.flush()
# Publish to specific subjects
await nc.publish("orders.wholesale.placed", b"Order W73737")
await nc.publish("orders.retail.placed", b"Order R65432")
await nc.publish("orders.wholesale.shipped", b"Order W73001")
await nc.publish("orders.retail.shipped", b"Order R65321")
// Subscribe to the shipped orders
Dispatcher dShipped = nc.createDispatcher(msg -> {
System.out.printf("[orders.*.shipped] %s (%s)\n", new String(msg.getData()), msg.getSubject());
});
dShipped.subscribe("orders.*.shipped");
Dispatcher dPlaced = nc.createDispatcher(msg -> {
System.out.printf("[orders.*.placed] %s (%s)\n", new String(msg.getData()), msg.getSubject());
});
dPlaced.subscribe("orders.*.placed");
// Subscribe to the retail orders
Dispatcher dRetail = nc.createDispatcher(msg -> {
System.out.printf("[orders.retail.*] %s (%s)\n", new String(msg.getData()), msg.getSubject());
});
dRetail.subscribe("orders.retail.*");
// Publish messages to the various subjects
nc.publish("orders.wholesale.placed", "Order W73737".getBytes());
nc.publish("orders.retail.placed", "Order R65432".getBytes());
nc.publish("orders.wholesale.shipped", "Order W73001".getBytes());
nc.publish("orders.retail.shipped", "Order R65321".getBytes());
// Subscribe with single token wildcard
let mut sub1 = client.subscribe("orders.*.shipped").await?;
tokio::spawn(async move {
while let Some(msg) = sub1.next().await {
println!(
"[orders.*.shipped] {} ({})",
String::from_utf8_lossy(&msg.payload),
msg.subject
);
}
});
let mut sub2 = client.subscribe("orders.*.placed").await?;
tokio::spawn(async move {
while let Some(msg) = sub2.next().await {
println!(
"[orders.*.placed] {} ({})",
String::from_utf8_lossy(&msg.payload),
msg.subject
);
}
});
let mut sub3 = client.subscribe("orders.retail.*").await?;
tokio::spawn(async move {
while let Some(msg) = sub3.next().await {
println!(
"[orders.retail.*] {} ({})",
String::from_utf8_lossy(&msg.payload),
msg.subject
);
}
});
// Publish to specific subjects
client
.publish("orders.wholesale.placed", "Order W73737".into())
.await?;
client
.publish("orders.retail.placed", "Order R65432".into())
.await?;
client
.publish("orders.wholesale.shipped", "Order W73001".into())
.await?;
client
.publish("orders.retail.shipped", "Order R65321".into())
.await?;
// Subscribe to the shipped orders
_ = Task.Run(async () =>
{
await foreach (var msg in client.SubscribeAsync<string>("orders.*.shipped"))
{
output.WriteLine($"[orders.*.shipped] {msg.Data,-12} ({msg.Subject})");
}
});
_ = Task.Run(async () =>
{
await foreach (var msg in client.SubscribeAsync<string>("orders.*.placed"))
{
output.WriteLine($"[orders.*.placed] {msg.Data,-12} ({msg.Subject})");
}
});
// Subscribe to the retail orders
_ = Task.Run(async () =>
{
await foreach (var msg in client.SubscribeAsync<string>("orders.retail.*"))
{
output.WriteLine($"[orders.retail.*] {msg.Data,-12} ({msg.Subject})");
}
});
// Let subscription tasks start
await Task.Delay(1000);
// Publish to specific subjects
await client.PublishAsync("orders.wholesale.placed", "Order W73737");
await client.PublishAsync("orders.retail.placed", "Order R65432");
await client.PublishAsync("orders.wholesale.shipped", "Order W73001");
await client.PublishAsync("orders.retail.shipped", "Order R65321");
Multi-Token Wildcard (>)
The > wildcard matches one or more tokens and can only appear at the end of a subject.
If your domain is like this:
sensor.alarm.smoke # unqualified
sensor.alarm.smoke.critical # qualified
sensor.alarm.water
sensor.alarm.water.critical
The > wildcard matches one or more tokens and can only appear at the end of a subject.
For example, sensor.> matches all sensor subjects
- CLI
- JavaScript/TypeScript
- Go
- Python
- Java
- Rust
- C#/.NET
# Subscribe using single token wildcard.
# Since each sub waits indefinitely, try each sub
# in a different terminal or just repeat the
# publishes for each sub.
nats sub "sensor.alarm.*"
nats sub "sensor.*.*.critical"
nats sub "sensor.>"
# Publish to specific subjects (use a different terminal)
nats pub sensor.alarm.smoke "kitchen,14:22"
nats pub sensor.alarm.smoke.critical "kitchen,14:23"
nats pub sensor.alarm.water "basement,16:42"
nats pub sensor.alarm.water.critical "basement,16:43"
async function process(subject: string) {
const sub = nc.subscribe(subject);
const label = `[${subject}]`.padEnd(23);
for await (const msg of sub) {
console.log(`${label}${msg.string().padEnd(15)} (${msg.subject})`);
}
}
// Subscribe to all alarms
process("sensor.alarm.*").catch(console.error);
// Subscribe to all critical
process("sensor.*.*.critical").catch(console.error);
// Subscribe to everything
process("sensor.>").catch(console.error);
// Publish to specific subjects
nc.publish("sensor.alarm.smoke", "kitchen,14:22");
nc.publish("sensor.alarm.smoke.critical", "kitchen,14:23");
nc.publish("sensor.alarm.water", "basement,16:42");
nc.publish("sensor.alarm.water.critical", "basement,16:43");
// Subscribe to all alarms
nc.Subscribe("sensor.alarm.*", func(m *nats.Msg) {
fmt.Printf("[sensor.alarm.*] %-15s (%s)\n", string(m.Data), m.Subject)
})
// Subscribe to all critical
nc.Subscribe("sensor.*.*.critical", func(m *nats.Msg) {
fmt.Printf("[sensor.*.*.critical] %-15s (%s)\n", string(m.Data), m.Subject)
})
// Subscribe to everything
nc.Subscribe("sensor.>", func(m *nats.Msg) {
fmt.Printf("[sensor.>] %-15s (%s)\n", string(m.Data), m.Subject)
})
// Publish to specific subjects
nc.Publish("sensor.alarm.smoke", []byte("kitchen,14:22"))
nc.Publish("sensor.alarm.smoke.critical", []byte("kitchen,14:23"))
nc.Publish("sensor.alarm.water", []byte("basement,16:42"))
nc.Publish("sensor.alarm.water.critical", []byte("basement,16:43"))
# Subscribe to all alarms
sub_alarm = await nc.subscribe("sensor.alarm.*")
# Subscribe to all critical
sub_critical = await nc.subscribe("sensor.*.*.critical")
# Subscribe to everything under sensor
sub_all = await nc.subscribe("sensor.>")
async def reader(sub, label):
async for msg in sub:
print(f"[{label:<22}] {msg.data.decode():<15} ({msg.subject})")
asyncio.create_task(reader(sub_alarm, "sensor.alarm.*"))
asyncio.create_task(reader(sub_critical, "sensor.*.*.critical"))
asyncio.create_task(reader(sub_all, "sensor.>"))
await nc.flush()
# Publish to specific subjects
await nc.publish("sensor.alarm.smoke", b"kitchen,14:22")
await nc.publish("sensor.alarm.smoke.critical", b"kitchen,14:23")
await nc.publish("sensor.alarm.water", b"basement,16:42")
await nc.publish("sensor.alarm.water.critical", b"basement,16:43")
public static void main(String[] args) {
try (Connection nc = Nats.connect("demo.nats.io")) {
// Subscribe to all alarms
Dispatcher dShipped = nc.createDispatcher(msg -> {
System.out.printf("[sensor.alarm.*] %-15s (%s)\n", new String(msg.getData()), msg.getSubject());
});
dShipped.subscribe("sensor.alarm.*");
// Subscribe to the all critical
Dispatcher dPlaced = nc.createDispatcher(msg -> {
System.out.printf("[sensor.*.*.critical] %-15s (%s)\n", new String(msg.getData()), msg.getSubject());
});
dPlaced.subscribe("sensor.*.*.critical");
// Subscribe to everything
Dispatcher dRetail = nc.createDispatcher(msg -> {
System.out.printf("[sensor.>] %-15s (%s)\n", new String(msg.getData()), msg.getSubject());
});
dRetail.subscribe("sensor.>");
// Publish messages to the various subjects
nc.publish("sensor.alarm.smoke", "kitchen,14:22".getBytes());
nc.publish("sensor.alarm.smoke.critical", "kitchen,14:23".getBytes());
nc.publish("sensor.alarm.water", "basement,16:42".getBytes());
nc.publish("sensor.alarm.water.critical", "basement,16:43".getBytes());
// Subscribe to all alarms
let mut sub1 = client.subscribe("sensor.alarm.*").await?;
tokio::spawn(async move {
while let Some(msg) = sub1.next().await {
println!(
"[sensor.alarm.*] {:15} ({})",
String::from_utf8_lossy(&msg.payload),
msg.subject
);
}
});
// Subscribe to the all critical
let mut sub2 = client.subscribe("sensor.*.*.critical").await?;
tokio::spawn(async move {
while let Some(msg) = sub2.next().await {
println!(
"[sensor.*.*.critical] {:15} ({})",
String::from_utf8_lossy(&msg.payload),
msg.subject
);
}
});
// Subscribe to everything
let mut sub3 = client.subscribe("sensor.>").await?;
tokio::spawn(async move {
while let Some(msg) = sub3.next().await {
println!(
"[sensor.>] {:15} ({})",
String::from_utf8_lossy(&msg.payload),
msg.subject
);
}
});
// Publish to specific subjects
client
.publish("sensor.alarm.smoke", "kitchen,14:22".into())
.await?;
client
.publish("sensor.alarm.smoke.critical", "kitchen,14:23".into())
.await?;
client
.publish("sensor.alarm.water", "basement,16:42".into())
.await?;
client
.publish("sensor.alarm.water.critical", "basement,16:43".into())
.await?;
// Subscribe to all non-critical alarms
_ = Task.Run(async () =>
{
await foreach (var msg in client.SubscribeAsync<string>("sensor.alarm.*"))
{
output.WriteLine($"[sensor.alarm.*] {msg.Data,-15} ({msg.Subject})");
}
});
// Subscribe to all critical
_ = Task.Run(async () =>
{
await foreach (var msg in client.SubscribeAsync<string>("sensor.*.*.critical"))
{
output.WriteLine($"[sensor.*.*.critical] {msg.Data,-15} ({msg.Subject})");
}
});
// Subscribe to everything
_ = Task.Run(async () =>
{
await foreach (var msg in client.SubscribeAsync<string>("sensor.>"))
{
output.WriteLine($"[sensor.>] {msg.Data,-15} ({msg.Subject})");
}
});
// Let subscription tasks start
await Task.Delay(1000);
// Publish to specific subjects
await client.PublishAsync("sensor.alarm.smoke", "kitchen,14:22");
await client.PublishAsync("sensor.alarm.smoke.critical", "kitchen,14:23");
await client.PublishAsync("sensor.alarm.water", "basement,16:42");
await client.PublishAsync("sensor.alarm.water.critical", "basement,16:43");
Wildcard Comparison
You can combine wildcards for more complex patterns and compare how * and > wildcards behave differently:
* (single token) vs > (multiple tokens) wildcardsThe visualization demonstrates:
- Single token wildcard
*: Matches exactly one token, as insensor.alarm.*andsensor.*.*.critical - Multi-token wildcard
>: Matches one or more tokens, as insensor.>
Subject Naming Conventions
Recommended Characters
- Alphanumeric:
a-z,A-Z,0-9 - Special:
-(dash) and_(underscore) - Delimiter:
.(dot) for hierarchy
Reserved Characters
.(dot) - Used for hierarchy, cannot be part of a token*(asterisk) - Wildcard, cannot be in subject names>(greater than) - Wildcard, cannot be in subject names- Whitespace - Not allowed in subjects
Reserved Prefixes
Subjects starting with $ are reserved for system use:
$SYS- System subjects$JS- JetStream API subjects$KV- Key-Value store subjects$O- Object Store subjects$SRV- Service API subjects_INBOX- Auto-generated reply subjects
Best Practices
Subject Hierarchy Design
-
Start general, get specific: Use the first tokens for broad categorization
app.region.service.entity.action
myapp.us-east.users.profile.update -
Keep it reasonable: Limit to ~16 tokens and under 256 characters total
-
Be consistent: Establish naming conventions early and stick to them
-
Plan for wildcards: Design hierarchies that work well with wildcard subscriptions
Performance Considerations
- Subjects Interest graph is in-memory and dynamic: NATS builds a routing table only for subjects with active subscribers, kept entirely in RAM for fast lookups
- Subjects are essentially free: Creating new subjects has virtually no overhead - NATS efficiently handles millions of unique subjects.
- Wildcard matching is optimized: Subscriptions with wildcards (
*and>) use efficient trie-based matching.
Security and Filtering
Well-designed subject hierarchies enable:
- Fine-grained access control per user/account
- Efficient message filtering in JetStream streams
- Clean import/export patterns between accounts
- Logical organization for monitoring and debugging
Location Transparency
One of NATS' key features is location transparency through subject-based addressing:
- Subscriptions automatically propagate across the NATS cluster
- Messages route to all interested subscribers regardless of their location
- No configuration needed for message routing between servers
- Publishers and subscribers don't need to know about each other's location
Wire Taps and Monitoring
The > wildcard enables powerful monitoring capabilities:
- CLI
- JavaScript/TypeScript
- Go
- Python
- Java
- Rust
- C#/.NET
# Monitor all messages in the system (subject to permissions)
nats sub ">"
# Monitor all orders
nats sub "orders.>"
# Monitor specific service communications
nats sub "myservice.>"
// Create a wire tap for monitoring
const sub = nc.subscribe(">");
(async () => {
for await (const msg of sub) {
console.log(`[MONITOR] ${msg.subject}: ${msg.string()}`);
}
})().catch(console.error);
// Create a wire tap for monitoring
nc.Subscribe(">", func(m *nats.Msg) {
fmt.Printf("[MONITOR] %s: %s\n", m.Subject, string(m.Data))
})
# Create a wire tap for monitoring - subscribe to everything
sub = await nc.subscribe(">")
received = asyncio.Event()
seen = 0
async def monitor():
nonlocal seen
async for msg in sub:
seen += 1
print(f"[MONITOR] {msg.subject} --> {msg.data.decode()}")
if seen >= 3:
received.set()
asyncio.create_task(monitor())
await nc.flush()
# Publish a message to various subjects
await nc.publish("hello", b"Hello NATS!")
await nc.publish("event.new", b"click")
await nc.publish("weather.north.fr", "Temperature: 11°C".encode())
print("Waiting for messages...")
await received.wait()
// Asynchronous subscribers require a dispatcher
// Subscribe to everything
CountDownLatch latch = new CountDownLatch(3);
Dispatcher dEverything = nc.createDispatcher(msg -> {
latch.countDown();
System.out.println("[MONITOR] " +
msg.getSubject() + " --> " +
new String(msg.getData(), StandardCharsets.UTF_8));
});
dEverything.subscribe(">");
// Create a wire tap for monitoring
let mut sub = client.subscribe(">").await?;
tokio::spawn(async move {
while let Some(msg) = sub.next().await {
println!(
"[MONITOR] {}: {}",
msg.subject,
String::from_utf8_lossy(&msg.payload)
);
}
});
// Publish to a few subjects so the monitor has something to print
client.publish("orders.new", "Order 1".into()).await?;
client
.publish("sensor.alarm.smoke", "kitchen".into())
.await?;
client
.publish("billing.invoice.paid", "INV-42".into())
.await?;
// Wire tap: subscribe to everything for monitoring
await foreach (var msg in client.SubscribeAsync<string>(">"))
{
output.WriteLine($"[MONITOR] {msg.Subject}: {msg.Data}");
}
Related Concepts
- Publish-Subscribe Basics - Core messaging patterns
- Request-Reply - Synchronous communication using subjects
- Queue Groups - Load balancing with subject subscriptions
Try It Yourself
Experiment with subjects using the NATS CLI:
# Terminal 1: Subscribe with wildcards
nats sub "demo.>"
# Terminal 2: Publish to various subjects
nats pub demo.test "Hello"
nats pub demo.test.nested "Nested message"
nats pub demo.another.topic "Another topic"
Each message published in Terminal 2 will be received by the wildcard subscription in Terminal 1, demonstrating how subject hierarchies and wildcards work together.