NATS: The Cloud-Native Nervous System
Most messaging systems are shaped like the problem they were designed to solve in a specific decade. RabbitMQ was shaped by enterprise Java and AMQP routing needs circa 2007. Kafka was shaped by LinkedIn’s append-only log problem circa 2011. Both are excellent at what they were designed for, and both carry the operational weight of that origin — broker clusters, coordination services, schema registries.
NATS came from a different angle. Apcera built it in 2010 for platform-as-a-service orchestration, where you needed every process talking to every other process with minimum overhead and zero configuration drift. The result is a server that compiles to a single Go binary with no external dependencies, boots in milliseconds, and routes messages through a subject hierarchy clean enough to draw on a whiteboard. Over time NATS grew a full persistence and streaming layer (JetStream), a distributed KV and object store, a leaf-node topology for edge deployments, and a cryptographic security model built on NaCl keypairs. It remains a single binary.
NATS is the right nervous system for cloud-native architectures where you want messaging everywhere — across services, across regions, out to edge nodes — without the operational gravity of a separate Kafka cluster per environment. It is not always the right tool, and the post will say so plainly.
The model: subjects, not queues
NATS routes messages by subject — a dot-delimited string like orders.created, sensors.eu-west.temp, or auth.token.refresh. Publishers send to a subject; subscribers declare what subjects they care about. There is no queue object to provision, no exchange to configure, no binding to declare. The subject is the address.
# publish (nats CLI)
nats pub orders.created '{"id": 42, "sku": "A1"}'
# subscribe to one exact subject
nats sub orders.created
# subscribe with wildcards
nats sub 'orders.*' # single token: orders.created, orders.cancelled, ...
nats sub 'sensors.>' # multi-token: sensors.eu-west.temp, sensors.us-east.pressure, ...
Two wildcard tokens: * matches one subject token; > matches one or more tokens and must appear at the end. The hierarchy is the primary tool for scoped, multi-tenant routing with no broker-side configuration.
Subject hierarchy example
─────────────────────────
platform
├── orders
│ ├── created ← nats sub 'platform.orders.*' catches this
│ ├── updated
│ └── cancelled
└── billing
└── invoice.paid ← nats sub 'platform.>' catches everything
Core NATS: fire and forget, at-most-once
Out of the box, NATS is at-most-once, fire-and-forget. The server delivers a message to all current subscribers and moves on. There is no durability, no disk, no replay. If a subscriber is down when a message arrives, it misses it. Full stop.
This is a feature, not a design failure. Sub-millisecond latency, no disk I/O, no state to manage. The right model for metrics, health signals, and live telemetry where gaps are tolerable. The wrong model for anything you cannot afford to lose — that is JetStream’s job.
Request/reply
NATS has built-in request/reply without a separate RPC framework. A requester calls request(), which creates a temporary inbox subject, publishes the message with the inbox as the reply-to field, and blocks for a response. A responder subscribes to the service subject and publishes back to msg.reply.
|
|
No service mesh required. Services discover each other by subject name and respond directly, composing naturally with queue groups for load-balanced responders.
Queue groups
When you need multiple instances of a service to share work without each receiving every message, you use a queue group. All subscribers that join the same named queue group receive only one delivery per message — the server picks a member at random. This is horizontal load balancing with zero configuration beyond the group name.
Without queue group: With queue group "workers":
───────────────────── ──────────────────────────
publisher ──▶ sub A publisher ──▶ server
──▶ sub B │
──▶ sub C (picks one)
(all three receive it) ▼
sub A (B and C get nothing)
Scale a worker service from one instance to ten: add nine more processes with the same queue group name, no partition reassignment. This is one of the more elegant pieces of the NATS design.
JetStream: persistence, at-least-once, and the rest
Core NATS routes messages; JetStream stores them. It is a persistence layer built into the same binary — flip jetstream: true in nats-server.conf and it activates. No separate process, no external storage dependency beyond a local directory.
The fundamental objects are streams and consumers. A stream is a persistent, ordered log on one or more subjects with configurable retention (by count, size, or age), replication factor, and storage type (file or memory). Messages published to any matching subject are captured automatically.
|
|
A consumer is a cursor into that stream with a delivery policy attached. Push consumers have the server deliver to a subject; pull consumers let the client fetch on its own schedule (better for horizontal worker scaling). Durable consumers track state server-side and survive client restarts, enabling at-least-once delivery: the server redelivers until the consumer sends an ack. Ephemeral consumers carry no server state and are cleaned up on disconnect.
For exactly-once delivery, JetStream provides a server-side dedup window: the publisher includes a unique Nats-Msg-Id header and the server discards duplicates within the configured window (default ten seconds). Combine that with consumer double-ack and you get end-to-end exactly-once for the common case — with the standard caveat that a partitioned network can still produce observable duplicates.
JetStream architecture
──────────────────────
publishers ──▶ subjects ──▶ ┌─────────────┐
│ Stream │ (persisted log, replicated via Raft)
└──────┬──────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
consumer A consumer B consumer C
(push, live) (pull, batch) (replay from beginning)
The KV store
JetStream exposes a key-value store built on top of streams. Each bucket is a stream; each put is a message; each key revision is retained. You get immediately consistent reads, optimistic concurrency control (update a key only if its revision matches), and TTL expiry per key. The watch API lets clients subscribe to changes on a key or prefix — reactive configuration without polling.
|
|
This is not Redis. But for service configuration, feature flags, distributed locks, and leader election — backed by the same Raft consensus as your message streams — it is compelling precisely because it is already running in your NATS cluster. See the redis-beyond-caching post for where Redis’s richer data structures still win.
The object store
The object store chunks binary assets into messages, replicates them via the stream mechanism, and presents a get/put API. Not S3 — no presigned URLs, no lifecycle policies. But for distributing artifacts within a cluster or to edge leaf nodes, it removes an external dependency without introducing a new process.
Leaf nodes and superclusters: messaging without borders
NATS clusters scale vertically with more memory and horizontally with more nodes, but the genuinely distinctive topology feature is the leaf node. A leaf node is a NATS server that connects outbound to a hub cluster (or supercluster) over a single connection and acts as a local NATS server for processes nearby — a remote office, an edge device, a Kubernetes cluster in another region.
Global topology
───────────────
┌─────────────────── Supercluster ────────────────────┐
│ cluster US-EAST ◀──── routes ────▶ cluster EU-WEST │
└──────────┬────────────────────────────────┬───────────┘
│ leaf connection │ leaf connection
▼ ▼
leaf: factory-floor leaf: retail-store-42
(local sensors, PLCs) (local POS, inventory)
Processes connected to a leaf node get sub-millisecond local pub/sub; the leaf selectively bridges subjects to the hub based on its import/export config. A factory floor sensor exports only aggregated events, never raw data, to the cloud. A leaf can survive a hub disconnect and continue operating locally with JetStream, syncing when connectivity returns.
Superclusters connect clusters via gateway links across regions. Unlike a stretched single cluster, a supercluster only forwards a message to a remote cluster if something there is actually subscribed — interest-based optimization that avoids broadcasting every event globally.
Security: accounts, NKEYS, and JWTs
NATS’s security model is not bolted on. It is built around NaCl Ed25519 keypairs (called NKEYS) at every level of the hierarchy.
Trust hierarchy
───────────────
Operator keypair (controls the deployment)
└── Account keypairs (isolate teams / applications)
└── User keypairs (individual clients)
An account is an isolated messaging namespace. Subjects in one account are invisible to another account by default. Accounts share subjects only through explicit export/import declarations — account A exports metrics.>, account B imports it under a local alias. This is multi-tenancy without running separate servers.
NKEYS sign JWTs that carry account and user permissions. An operator signs account JWTs; accounts sign user JWTs; the server validates the chain and grants encoded permissions. Credentials rotate without a server restart — push a new user JWT to the resolver and it takes effect on next connection. An April 2025 OSTIF security audit found no critical vulnerabilities, with a recommended tightening (disabling bearer JWT at the account level) implemented in the same release cycle.
|
|
Plain username/password and TLS client certificates are also supported for simpler deployments. The NKEYS/JWT path is the right choice for any multi-tenant or production deployment.
NATS vs Kafka vs RabbitMQ
This comparison comes up constantly, and the honest version is that they solve overlapping but distinct problems.
| Dimension | NATS | Kafka | RabbitMQ |
|---|---|---|---|
| Operational footprint | Single binary, no ZooKeeper, no schema registry | Kafka 4.0+ uses KRaft (no ZooKeeper), but still a JVM cluster + broker management | Erlang runtime, management plugin, plugins for features |
| Latency (persistent) | 1–5ms (JetStream, file storage) | 10–50ms (batching by design) | 5–20ms (AMQP with durability) |
| Throughput | 200–400K msg/s (JetStream) | 500K–1M+ msg/s (batching) | 50–100K msg/s |
| Message retention | Configurable per stream; not a log-first design | Log-first, indefinite retention, replayable | Queue-centric; messages consumed and gone |
| Routing | Subject wildcards, account import/export | Topics and partitions; no content-based routing | Exchanges, bindings, headers routing — richest in the class |
| Multi-tenancy | First-class via accounts | Bolted on via ACLs and quotas | Virtual hosts, limited isolation |
| Edge / leaf topology | First-class leaf nodes | Not designed for it | Not designed for it |
| Ecosystem | Growing; strong in Go/cloud-native space | Enormous; Kafka Connect, ksqlDB, Flink, Spark | Mature; broad language support, AMQP interop |
Where NATS wins: you want a single operational primitive for pub/sub, request/reply, work queues, KV, and object store across a heterogeneous environment including edge. A NATS cluster in Kubernetes is a three-line Helm install; developer laptops run a local cluster with nats-server -js; CI spins up an in-process test server in Go with no external dependency. That operational surface is genuinely smaller than anything Kafka or RabbitMQ offers.
Where Kafka wins: you need the immutable, replayable log as a source of truth. Kafka’s log retention lets you replay months of events through new pipeline versions, and the ecosystem — Kafka Connect, ksqlDB, Flink, tiered S3 storage, the schema registry — has no NATS equivalent. The apache-kafka-deep-dive post covers this in detail. For event sourcing and stream-processing pipelines, Kafka is the right tool and the operational overhead is the price of that capability.
Where RabbitMQ wins: AMQP protocol interoperability, or complex exchange-based routing — fanout, topic exchanges with sophisticated binding patterns, dead-letter queues with per-message TTL, priority queues. NATS subject wildcards cover most routing scenarios, but RabbitMQ’s exchange/binding model is more expressive for complex enterprise topologies and legacy AMQP integration.
When NOT to use NATS
NATS fits an enormous range of problems, which makes it tempting to reach for it everywhere. Resist.
Skip NATS when your core use case is log analytics and event sourcing. If you need months of replay, tight Flink/Spark integration, and Kafka Connect sinks to a data warehouse, build on Kafka. JetStream streams are not the same as Kafka partitions, and the ecosystem gap is real.
Skip NATS when you need AMQP compatibility or complex broker-side routing. RabbitMQ’s exchange model — direct, fanout, topic, headers routing — is more expressive than subject wildcards for complex enterprise topologies. Do not rebuild a dead-letter queue in JetStream when RabbitMQ has it natively.
Skip NATS if your team’s operational knowledge is deep in one of the alternatives. Operational familiarity is a real asset. A team that has run Kafka for three years will make fewer mistakes on Kafka than on a system they are learning under production load.
Skip NATS for very large binary streaming. The object store handles medium-sized blobs, but it is not a replacement for S3 or a dedicated media pipeline. Max message size defaults to 8 MB.
Skip NATS if you need multi-region strong consistency for a database-like workload. JetStream’s Raft consensus is per-cluster. Cross-region reads via leaf nodes are eventually consistent unless you route to the stream’s Raft leader — fine for messaging, wrong for synchronous cross-region KV consistency.
The verdict
NATS earned its “nervous system” framing. Subject-based routing, built-in request/reply, queue groups, JetStream persistence with KV and object store, leaf nodes for edge topologies, cryptographic multi-tenant security — one binary, zero external dependencies, boots in under a second. The operational contrast with a Kafka cluster or a RabbitMQ deployment is not subtle.
The places where it is not the right tool are real and worth respecting: Kafka for log-first analytics and the stream-processing ecosystem, RabbitMQ for AMQP interoperability and complex exchange routing. Those are not arguments against NATS — they are arguments for matching the tool to the actual problem.
The clearest signal to reach for NATS: you want pub/sub, work queues, request/reply, and a lightweight KV store, and you do not want to run four separate systems to get them. One binary, one operational surface, no zoo.
Sources
- NATS JetStream concepts and architecture — NATS Docs
- Compare NATS — official comparison table — NATS Docs
- NATS Queue Groups — NATS Docs
- NATS JetStream Key/Value Store — NATS Docs
- NATS JWT security in depth — NATS Docs
- NATS Security Self-Assessment (OSTIF audit, April 2025)
- NATS JetStream persistence guide — OneUptime Blog, January 2026
- NATS vs Apache Kafka vs RabbitMQ: Messaging Showdown — sanj.dev
- RabbitMQ vs NATS vs Kafka benchmark on VPS — Onidel, 2025
- Message Brokers Comparison 2026 — DEV Community
Comments