Install
# Linux binary
NATS_VER=2.10.21
curl -L "https://github.com/nats-io/nats-server/releases/download/v${NATS_VER}/nats-server-v${NATS_VER}-linux-amd64.tar.gz" \
| tar -xz
sudo cp nats-server-v${NATS_VER}-linux-amd64/nats-server /usr/local/bin/
# Plus the nats CLI
NATSCLI_VER=0.1.5
curl -L "https://github.com/nats-io/natscli/releases/download/v${NATSCLI_VER}/nats-${NATSCLI_VER}-linux-amd64.zip" \
-o /tmp/nats.zip
unzip /tmp/nats.zip -d /tmp
sudo install /tmp/nats-${NATSCLI_VER}-linux-amd64/nats /usr/local/bin/
# macOS via Homebrew
brew install nats-server nats-io/nats-tools/nats
Single-node JetStream-enabled server
# /etc/nats/nats.conf
port: 4222
http_port: 8222
jetstream {
store_dir: /var/lib/nats/jetstream
max_memory_store: 1GB
max_file_store: 50GB
}
server_name: nats-1
# Optional: leaf node connection to a remote cluster (e.g. ngs.global.synadia.com)
# leafnodes {
# remotes = [ { url: "tls://connect.ngs.global:7422" } ]
# }
sudo systemctl enable --now nats-server
nats server check connection
Plain pub/sub
NATS routes by subject — a dot-separated hierarchical string (orders.new, logs.app.error, events.user.signup). Subscribers can wildcard:
# Terminal A — subscribe
nats sub 'orders.*' # matches one level: orders.new, orders.cancelled
# Or
nats sub 'logs.>' # matches arbitrary depth: logs.app.error.connection
# Terminal B — publish
nats pub orders.new '{"id":42,"amount":100}'
Plain NATS is at-most-once: subscribers connected at publish time get the message; others don't.
Request/reply
# Server: respond to math.add requests
nats reply 'math.add' '--command' 'echo $((NATS_REQUEST))'
# Client: request
nats request math.add "1+2"
Built-in pattern; no separate RPC framework needed.
JetStream: persistent streams
Plain NATS is in-memory; JetStream adds persistence + at-least-once delivery + replay.
# Create a stream that captures the orders.* subject space
nats stream add ORDERS --subjects 'orders.*' \
--storage file --retention limits --max-age 7d --max-bytes 10GB \
--max-msgs -1 --max-msg-size 1MB --replicas 1 \
--discard old --dupe-window 2m \
--no-allow-rollup --no-deny-delete --no-deny-purge
# Publish into it (any pub to orders.* now persists)
nats pub orders.new '{"id":42,"amount":100}'
# Browse messages
nats stream view ORDERS
nats stream report
Durable consumers
# Create a durable pull consumer
nats consumer add ORDERS billing-worker \
--pull --filter 'orders.*' \
--deliver all --ack explicit --max-deliver 5
# Worker pulls and acks
nats consumer next ORDERS billing-worker --count 10 --ack
If the worker crashes mid-process, the un-acked message redelivers (up to --max-deliver times, then dead-letters). Standard work-queue semantics.
JetStream KV: a clustered key-value store
nats kv add SETTINGS --history 5 --replicas 3 --max-bucket-size 100MB
nats kv put SETTINGS feature_x_enabled true
nats kv get SETTINGS feature_x_enabled
nats kv ls SETTINGS
# Watch for changes (push-style)
nats kv watch SETTINGS
Per-key history is configurable; watch-style change notifications come from the same connection that does the regular KV ops. Useful for distributed feature flags, dynamic configuration, leader election (via the --ttl / heartbeat pattern).
JetStream Object Store
# Create an object-store bucket
nats object add ASSETS --max-bucket-size 10GB
# Put / get / list
nats object put ASSETS ./logo.svg
nats object get ASSETS logo.svg --output /tmp/logo-restored.svg
nats object ls ASSETS
Backed by a JetStream stream under the hood. For "small object storage that's clustered with the same operational story as my messaging," this avoids standing up a separate S3-compatible service.
Clustering for HA
Three nodes, RAFT-based replication:
# Each node's nats.conf
server_name: nats-1 # nats-1 / nats-2 / nats-3
cluster {
port: 6222
routes = [
nats-route://nats-1.lab:6222
nats-route://nats-2.lab:6222
nats-route://nats-3.lab:6222
]
}
jetstream {
store_dir: /var/lib/nats/jetstream
}
Restart all three. nats server list shows the cluster. Streams / KV / Object Stores created with --replicas 3 get RAFT consensus + per-node replicas.
Auth + multi-tenant
NATS has a sophisticated account / user model. The simplest secure setup:
accounts {
APP: {
users: [
{ user: app-write, password: "$2a$11$..." }
{ user: app-read, password: "$2a$11$...", permissions: { subscribe: "orders.>", publish: { deny: ">" } } }
]
jetstream: enabled
}
SYS: {
users: [{ user: admin, password: "$2a$11$..." }]
}
}
system_account: SYS
Use bcrypt-hashed passwords (nats server passwd). For real production, use the NATS NKeys / JWT auth flow with a separate signing key — cryptographic identity per client, revocable centrally.
Bridges
NATS has first-class bridges into other systems:
- MQTT broker mode — the NATS server can speak MQTT 3.1.1 on a separate port. IoT devices publish MQTT; downstream consumers use NATS. (Cleaner than running both Mosquitto and NATS; see Mosquitto tutorial for the dedicated-broker route.)
- WebSocket support — browser clients connect via NATS-over-WebSocket, no separate gateway needed.
- Kafka mirror — the NATS-Kafka bridge replicates between the two.
NATS vs the rest
- vs Redis pub/sub — Redis is at-most-once + ephemeral; NATS plain is the same; JetStream adds persistence. NATS scales better cross-network.
- vs Kafka / Redpanda (see Redpanda) — Kafka is built for high-throughput append-only logs with strong ordering; NATS JetStream covers the same shape but at smaller per-message-throughput. Kafka wins at very-large scale; NATS wins at simpler ops.
- vs RabbitMQ — RabbitMQ has more elaborate routing (exchanges, bindings, AMQP topologies); NATS has flat subjects + filters. NATS is dramatically lighter operationally.
- vs MQTT — MQTT is purpose-built for IoT; NATS's MQTT mode covers it plus gives you the wider NATS ecosystem in the same broker.
For internal microservices messaging on a self-hosted infrastructure, NATS hits the sweet spot of "feature-rich enough to actually use, light enough to actually operate."