NATS isn’t just a message queue; it’s a distributed messaging system that can achieve sub-millisecond latency because its core design prioritizes simplicity and speed, often by deferring complex logic to clients rather than the server.

Let’s see it in action. Imagine you have a simple NATS server running.

nats-server -p 4222

Now, from one terminal, you can subscribe to a subject:

nats sub "updates.>" --count 5

This command tells the NATS CLI to listen for any messages published on subjects that start with "updates.". The --count 5 means it will exit after receiving five messages.

In another terminal, you can publish a message:

nats pub "updates.user.123" "Hello, User 123!"

And then another:

nats pub "updates.system.status" "Server is healthy."

The nats sub command will immediately display these messages as they arrive:

Received 1 messages on "updates.user.123":
Hello, User 123!
Received 2 messages on "updates.system.status":
Server is healthy.

This demonstrates the core publish-subscribe pattern. Publishers send messages to subjects, and subscribers interested in those subjects receive them. The NATS server acts as a smart router, forwarding messages efficiently.

The real power comes when you start thinking about how this scales and what kind of messages you can send. NATS supports various messaging patterns beyond simple publish-subscribe, including request-reply, queue groups, and streaming.

Request-reply is particularly useful for synchronous interactions. A service can publish a request and wait for a specific reply from a service that subscribes to that request subject and is configured to respond.

# In one terminal, start a service that listens for requests and replies
nats req "time.request" "What time is it?" --count 1

This nats req command will publish "What time is it?" to the "time.request" subject and wait for a single reply.

In another terminal, you can have a service that handles this request:

nats rep "time.request" "date +%T"

The nats rep command subscribes to "time.request", and for every message it receives, it executes the command date +%T and sends its output back as a reply. The original nats req command will then print the reply it received.

Queue groups are essential for distributing work. If multiple subscribers listen on the same subject with the same queue group name, only one subscriber in the group will receive any given message.

# Terminal 1
nats sub "jobs.process" --queue "worker.group" --count 1

# Terminal 2
nats sub "jobs.process" --queue "worker.group" --count 1

# Terminal 3
nats pub "jobs.process" "Task ABC"

When you publish "Task ABC", only one of the two nats sub commands will print the message. This is how you build scalable worker pools.

Debugging in NATS often involves understanding message flow and subject naming. A common pitfall is overly broad or overly specific subject wildcards. The > wildcard matches one or more subject components, while * matches exactly one.

For instance, updates.> will match updates.user.123 and updates.system.status, but updates.* would only match updates.user or updates.system if those subjects existed.

The NATS CLI offers a powerful --trace flag for observing detailed server-client interactions, which can be invaluable for diagnosing connectivity or routing issues.

nats sub "some.subject" --trace

This will show you the raw NATS protocol messages exchanged between the client and the server, including PINGs, PONGs, and message acknowledgments.

The most surprising thing about NATS’s performance is how little state it maintains by default. Unlike many traditional message brokers that need to track message delivery for every subscriber, NATS servers are primarily concerned with routing messages to active connections based on their subscriptions. This statelessness at the server level is a major contributor to its speed and scalability.

When you use --stream with NATS CLI, you’re interacting with NATS JetStream, the built-in persistence and streaming layer. JetStream adds durability and replayability to NATS, allowing you to configure message retention policies and consume messages even if your subscriber was offline.

Understanding the distinction between core NATS (fire-and-forget) and JetStream (persistent streams and key-value stores) is crucial for choosing the right tool for your use case. Core NATS is for high-throughput, low-latency messaging where occasional message loss is acceptable, while JetStream provides the guarantees needed for critical data processing and event sourcing.

The next concept you’ll encounter is managing JetStream streams and consumers, which involves defining storage configurations, retention policies, and understanding different acknowledgment strategies.

Want structured learning?

Take the full Nats course →