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 nats CLI: 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:

TokenMatchesExample
*Single tokenorders.*.placed matches orders.eu.placed
>All remaining tokensorders.> 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:

BackendUse case
filePersistence across restarts, large datasets
memoryUltra-low latency, cache-like workloads

Retention policies:

PolicyBehavior
limitsRetain until size/age/count limits are reached
interestRetain while at least one consumer exists
workqueueDelete 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

FeatureNATS+JetStreamKafkaRabbitMQRedis Pub/SubPulsarMQTT
LatencySub-msLow msLow msSub-msLow msSub-ms
ThroughputVery highVery highHighHighVery highMedium
PersistenceJetStreamYesYesNoYesOptional
Exactly-onceDedup windowTransactionsNoNoYesNo
Req/Reply nativeYesNoNoNoNoNo
KV StoreBuilt-inNoNoExternalNoNo
Ops complexityLowHighMediumLowHighLow
Edge/IoTLeaf nodesNoLimitedNoNoYes

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 --jetstream or set jetstream: {} in config. Without it, nats stream add fails 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 interest retention.
  • Pull consumers need explicit acks. If your code crashes before acking, max_deliver redeliveries 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_password in 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 /varz HTTP endpoint, nats-surveyor, and Grafana.