TL;DR — Quick Summary
NATS delivers ultra-fast cloud-native messaging and durable streaming. Learn pub/sub, JetStream, KV store, clustering, and auth for production microservices.
NATS is a connective technology built for cloud-native applications. A single binary with zero dependencies, it handles millions of messages per second at sub-millisecond latencies — then adds JetStream for durable streaming, Key-Value storage, and Object Store, all on the same infrastructure. This guide walks you through everything from basic pub/sub to production clustering.
Prerequisites
- Docker or a Linux host with wget/curl.
- The
natsCLI:curl -sf https://binaries.nats.dev/nats-io/natscli/nats@latest | sh - Basic familiarity with publish/subscribe messaging concepts.
- For Kubernetes: Helm 3 and a running cluster.
Step 1: Install nats-server
Docker (quickest)
# Single server with JetStream enabled
docker run -d --name nats \
-p 4222:4222 -p 8222:8222 \
nats:latest -js -m 8222
# Verify
nats server ping
Binary (Linux)
# Download latest release
curl -L https://github.com/nats-io/nats-server/releases/latest/download/nats-server-v2.10.14-linux-amd64.zip -o nats.zip
unzip nats.zip && sudo mv nats-server /usr/local/bin/
# Start with JetStream and monitoring
nats-server --jetstream --http_port 8222
Kubernetes (Helm)
helm repo add nats https://nats-io.github.io/k8s/helm/charts/
helm repo update
helm install nats nats/nats \
--set config.jetstream.enabled=true \
--set config.cluster.enabled=true \
--set replicaCount=3
Step 2: Core NATS — Pub/Sub, Request/Reply, Queue Groups
Core NATS is at-most-once: messages are delivered only to active subscribers. It is fast precisely because there is no persistence overhead.
Subject-Based Addressing
Subjects are hierarchical dot-delimited strings. Two wildcard tokens extend addressing power:
| Token | Matches | Example |
|---|---|---|
* | Single token | orders.*.placed matches orders.eu.placed |
> | All remaining tokens | orders.> matches orders.eu.placed, orders.us.updated |
# Terminal 1 — subscribe
nats sub "orders.>"
# Terminal 2 — publish
nats pub orders.us.placed '{"id":"1001","amount":99.00}'
nats pub orders.eu.updated '{"id":"1002","status":"shipped"}'
Queue Groups — Load Balancing
Add multiple subscribers to the same queue group and NATS delivers each message to exactly one member — no extra broker configuration required.
# Start two workers in queue group "processors"
nats sub --queue processors "orders.>" # terminal 1
nats sub --queue processors "orders.>" # terminal 2
# Messages are round-robined between the two workers
nats pub orders.us.placed '{"id":"1003"}'
Request/Reply Pattern
NATS has native request/reply with no-responders: if no service is listening, the caller receives an error immediately instead of timing out.
# Service (reply side)
nats reply "inventory.check" '{"available": true, "qty": 42}'
# Client (request side)
nats request "inventory.check" '{"sku":"WIDGET-A"}'
# → {"available": true, "qty": 42}
Step 3: JetStream — Durable Streaming
JetStream stores messages in streams. A stream captures messages matching one or more subjects and retains them according to a retention policy.
Create a Stream
# Interactive wizard
nats stream add ORDERS
# Or via flags
nats stream add ORDERS \
--subjects "orders.>" \
--storage file \
--retention limits \
--max-msgs 1000000 \
--max-age 7d \
--replicas 3
Storage backends:
| Backend | Use case |
|---|---|
file | Persistence across restarts, large datasets |
memory | Ultra-low latency, cache-like workloads |
Retention policies:
| Policy | Behavior |
|---|---|
limits | Retain until size/age/count limits are reached |
interest | Retain while at least one consumer exists |
workqueue | Delete each message after it is acknowledged by one consumer |
Push vs Pull Consumers
# Create a durable push consumer (auto-delivers to a subject)
nats consumer add ORDERS fulfillment \
--deliver-subject fulfillment.worker \
--ack explicit \
--durable fulfillment
# Create a durable pull consumer (client controls the rate)
nats consumer add ORDERS billing \
--pull \
--ack explicit \
--durable billing
# Pull a batch of 10 messages
nats consumer next ORDERS billing --count 10
Exactly-Once Delivery — Deduplication
JetStream deduplicates by Nats-Msg-Id header within a configurable window:
# Publish with a unique ID — duplicate IDs are silently dropped
nats pub orders.us.placed \
--header "Nats-Msg-Id: txn-abc-123" \
'{"id":"1004","amount":200}'
# Configure stream dedup window (default 2 minutes)
nats stream edit ORDERS --dupe-window 5m
Dead Letter — Max Deliver
When a consumer exhausts its max_deliver redelivery attempts, the message is sent to an advisory subject ($JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>). Subscribe to this subject or forward it to a dead-letter stream:
nats stream add DLQ \
--subjects "\$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.ORDERS.*" \
--storage file \
--retention limits
Step 4: Key-Value Store
The JetStream-backed KV store provides get/put/delete with history, TTL, and watch semantics — without running a separate Redis instance.
# Create a bucket
nats kv add CONFIG --history 10 --ttl 24h
# Write and read
nats kv put CONFIG db_url "postgres://prod-db:5432/app"
nats kv get CONFIG db_url
# Watch for changes (streams updates in real time)
nats kv watch CONFIG
# List all keys
nats kv ls CONFIG
Step 5: Object Store
For large binary assets (ML models, firmware images, backups) that must be distributed across nodes:
# Create a bucket with 3 replicas
nats object add ARTIFACTS --replicas 3
# Upload and download
nats object put ARTIFACTS ./model-v2.bin
nats object get ARTIFACTS model-v2.bin ./downloaded-model.bin
# List contents
nats object ls ARTIFACTS
Step 6: Authentication and Authorization
Token Auth (simplest)
nats-server --auth mysecrettoken
# Client
nats pub subject msg --creds '' --server nats://mysecrettoken@localhost:4222
NKeys — Cryptographic Identity
NKeys use Ed25519 key pairs. No passwords over the wire.
nk -gen user -pubout # generates seed + public key
# Add the public key to server config under authorization.users[]
Decentralized JWT Auth with nsc
For large deployments, use the operator → account → user JWT hierarchy:
# Bootstrap an operator
nsc add operator MyOrg
# Create an account and user
nsc add account AppA
nsc add user -a AppA alice
# Export credentials
nsc generate creds -a AppA -n alice > alice.creds
# Connect
nats pub subject msg --creds alice.creds
The server resolves JWTs from a built-in NATS-based resolver (resolver: NATS) — accounts are pushed as needed with nsc push.
Step 7: Clustering and Leaf Nodes
3-Node Cluster Configuration
# nats-server-1.conf
port: 4222
server_name: n1
jetstream: { store_dir: /data/nats }
cluster {
name: prod
port: 6222
routes: [
nats-route://nats-2:6222
nats-route://nats-3:6222
]
}
Start all three nodes and verify:
nats server report jetstream # shows meta-leader and stream placement
nats server list # lists all cluster members
Leaf Nodes for Edge Connectivity
A leaf node connects an edge site to the hub cluster over a single outbound TCP connection, reducing WAN traffic:
# leaf-site.conf
port: 4222
leafnodes {
remotes: [{ url: "nats://hub-cluster:7422" }]
}
Local edge services publish/subscribe normally; NATS handles routing to the hub only when needed.
Step 8: Monitoring
Built-in HTTP Endpoint
# Server stats
curl http://localhost:8222/varz | jq .
# Connection list
curl http://localhost:8222/connz | jq .
# Route info (cluster)
curl http://localhost:8222/routez | jq .
Prometheus Metrics
# Run nats-surveyor (scrape target for Prometheus)
docker run -d --name surveyor \
-p 7777:7777 \
natsio/nats-surveyor:latest \
--servers nats://nats:4222 \
--port 7777
Add http://surveyor:7777/metrics as a Prometheus scrape target. Import the NATS Grafana dashboard (ID 14725) for a pre-built overview of connections, subscriptions, JetStream streams, consumer lag, and throughput.
NATS vs Alternatives
| Feature | NATS+JetStream | Kafka | RabbitMQ | Redis Pub/Sub | Pulsar | MQTT |
|---|---|---|---|---|---|---|
| Latency | Sub-ms | Low ms | Low ms | Sub-ms | Low ms | Sub-ms |
| Throughput | Very high | Very high | High | High | Very high | Medium |
| Persistence | JetStream | Yes | Yes | No | Yes | Optional |
| Exactly-once | Dedup window | Transactions | No | No | Yes | No |
| Req/Reply native | Yes | No | No | No | No | No |
| KV Store | Built-in | No | No | External | No | No |
| Ops complexity | Low | High | Medium | Low | High | Low |
| Edge/IoT | Leaf nodes | No | Limited | No | No | Yes |
Practical Microservices Example
The following pattern wires together pub/sub, request/reply, and JetStream in a single application:
# 1. Create an orders stream (JetStream persistence)
nats stream add ORDERS --subjects "orders.>" --storage file --ack --replicas 1
# 2. Inventory service replies to stock checks (core NATS request/reply)
nats reply "inventory.check" '{"available":true}'
# 3. Order placement — publish to JetStream for durable processing
nats pub orders.us.placed \
--header "Nats-Msg-Id: $(uuidgen)" \
'{"id":"2001","sku":"WIDGET-A","qty":5}'
# 4. Fulfillment worker pulls from JetStream consumer
nats consumer add ORDERS fulfill --pull --ack explicit --durable fulfill
nats consumer next ORDERS fulfill --count 5
# 5. After processing, store order state in KV
nats kv put ORDERS "2001" '{"status":"fulfilled","ts":"2026-03-23T10:00:00Z"}'
This gives you: durable ingestion (no message lost if fulfilment is down), load-balanced workers (multiple pull consumer instances), exactly-once (Nats-Msg-Id dedup), and fast status lookups (KV read).
Gotchas and Edge Cases
- JetStream not enabled by default. Always pass
--jetstreamor setjetstream: {}in config. Without it,nats stream addfails silently on some clients. - Replicas require an odd number of nodes. A 2-node JetStream cluster cannot elect a meta-leader after one failure — always use 3 or more.
- Interest retention with no consumers deletes messages immediately. Create the consumer before publishing if using
interestretention. - Pull consumers need explicit acks. If your code crashes before acking,
max_deliverredeliveries kick in. Set this to a finite value and monitor the advisory subject. - Subject permissions in JWT auth are additive per account. A user in account A cannot publish to account B subjects unless explicit imports/exports are configured with
nsc. - Monitoring port 8222 is unauthenticated by default. Firewall it or use
http_username/http_passwordin config.
Summary
- Core NATS = at-most-once pub/sub, request/reply, queue groups — zero config, sub-ms latency.
- JetStream adds persistence, at-least-once/exactly-once delivery, consumers, KV, and Object Store.
- Use pull consumers for worker queues that need backpressure; push consumers for event fans.
- 3-node clusters are the minimum for JetStream HA; leaf nodes extend to the edge cheaply.
- nsc + JWT auth scales to hundreds of accounts with fine-grained subject permissions.
- Monitor via the
/varzHTTP endpoint, nats-surveyor, and Grafana.