NATS

NATS occupies an unusual position among messaging systems: it's a single Go binary with no dependencies, it starts in milliseconds, it handles millions of messages per second, and it covers a wider range of communication patterns than most alternatives. Core NATS gives you fire-and-forget pub/sub and — unusually — first-class request-reply, so it serves as both an event bus and an RPC transport. JetStream adds persistence, at-least-once and exactly-once delivery, replay, and consumer acknowledgement on top.

The design decision that shapes everything is subject-based addressing. There are no queues to declare, no exchanges to bind, no topics to provision. A publisher sends to orders.eu.created and a subscriber expresses interest in orders.*.created or orders.>. Routing is a subject-match at publish time, which is why the operational surface is so much smaller than a broker where topology is configuration.

The tradeoff worth stating up front: core NATS is at-most-once — if nobody is listening, the message is gone. That's a deliberate choice that keeps the hot path fast, and it means anything requiring durability needs JetStream.

TL;DR

Quick Example

The WithMsgID deduplication window (two minutes by default, configurable per stream) is what makes JetStream's exactly-once claim practical: a publisher that retries after a network timeout won't create a duplicate.

Core Concepts

Subjects

Subject design is the main design decision in a NATS system, and it functions as your API. A useful convention: <domain>.<entity>.<region-or-tenant>.<event> — most general to most specific, so subscribers can filter at the granularity they need and authorization can be expressed as subject prefixes.

The delivery models

This is the distinction that matters most when adopting NATS. Core NATS being at-most-once surprises people coming from RabbitMQ, where a durable queue is the default. It's not a limitation to work around — it's the right choice for metrics, presence, cache invalidation, and RPC, where a lost message is superseded by the next one and latency is what you care about.

Queue groups

This gives you pub/sub and competing consumers in one mechanism, without separate exchange and queue concepts. Adding a worker is starting a process with the same group name — no configuration change, no rebalance protocol.

JetStream consumers

Acknowledgement policies: AckExplicit (you ack each message — use this), AckAll (acking one acks everything before it), AckNone (fire and forget). Redelivery is governed by AckWait and MaxDeliver, with unacknowledged messages going to a dead-letter subject after the limit.

Retention policies

Combined with MaxAge, MaxBytes, MaxMsgs, and MaxMsgsPerSubject, plus Discard: Old|New to choose whether hitting a limit drops the oldest or rejects the newest.

MaxMsgsPerSubject: 1 deserves a mention — it turns a stream into a compacted per-key log, which is exactly how the KV store is built.

KV and object stores

The KV watch is the genuinely useful part: distributed configuration and feature flags with push-based invalidation, without adding etcd or Consul. It's not a replacement for a database — no queries, no secondary indexes — and it's excellent for the coordination cases where you'd otherwise reach for one.

Clustering and topology

Leaf nodes are the underrated feature. A device or an edge cluster connects out to the supercluster, gets the subjects it's interested in, and needs no inbound reachability — which makes NATS unusually good at edge and IoT topologies where the alternative is a VPN.

NATS vs. the Alternatives

NATS when you want low latency, operational simplicity, both messaging and RPC in one system, and edge or multi-region topology. Kafka when you need a long-retention replayable log, strong per-partition ordering, an ecosystem of connectors, or stream processing — it's a different tool despite the overlap. RabbitMQ when you need complex routing topologies, per-message TTL and priorities, or AMQP compatibility.

The honest framing: NATS is a messaging system with persistence added; Kafka is a distributed log with messaging semantics on top. Choosing between them is mostly about whether you need the log.

Best Practices

Design the subject hierarchy first

Subjects are your API and the hardest thing to change once services are deployed. Go general to specific, keep tokens meaningful, and put identity high enough in the tree that authorization can be expressed as prefix rules.

Use core NATS for ephemeral, JetStream for durable

Metrics, presence, cache invalidation, and RPC belong on core NATS — persistence is cost with no benefit when the next message supersedes this one. Orders, payments, and anything you'd miss belong on JetStream. Putting everything on JetStream is a common early mistake that gives up NATS's main advantage.

Prefer pull consumers

They give natural flow control, scale horizontally without configuration, and are the direction the project is heading. Push consumers are being deprecated.

Ack after the work is durable, not before

Publish with a message ID for idempotent producers

jetstream.WithMsgID(id) deduplicates retries within the stream's window. Without it, a publisher that times out and retries creates a duplicate — and the consumer has to handle it. See Idempotency.

Set MaxDeliver and a dead-letter subject

A message that fails forever will be redelivered forever, consuming capacity and filling logs. Bound the attempts and route exhausted messages somewhere a human can look at them.

Use Drain() rather than Close() on shutdown

Drain unsubscribes, processes what's already buffered, flushes pending publishes, and then closes. Close drops in-flight work. This matters on every rolling deploy.

Enable authorization with subject-scoped permissions

NATS accounts and NKeys/JWTs let you scope a client to publish and subscribe on specific subject patterns. Without it, any connected client can subscribe to > and read everything — a genuine problem in a multi-tenant or multi-team deployment.

Common Mistakes

Expecting core NATS to persist

Wildcards in a publish subject

Ephemeral consumers where durability is needed

No MaxDeliver

Treating JetStream as a database

Unbounded subject cardinality

FAQ

NATS or Kafka?

NATS when you want low latency, one binary to operate, request-reply alongside pub/sub, and edge or multi-region topology. Kafka when the log is the point: long retention, replay from arbitrary offsets, strong per-partition ordering, an ecosystem of connectors, or stream processing with Kafka Streams or Flink. If you'd describe your requirement as "an event log we'll reprocess," that's Kafka. If it's "services need to talk to each other quickly," that's NATS.

Is core NATS really at-most-once?

Yes, and deliberately. A published message is delivered to whoever is subscribed at that instant, with no buffering for absent subscribers and no acknowledgement. That's the right semantic for metrics, presence, cache invalidation, and RPC, and it's why core NATS latency is measured in microseconds. Anything you'd miss goes through JetStream.

Can NATS replace my HTTP RPC layer?

For internal service-to-service calls, often yes, and it's one of NATS's distinctive strengths. Request-reply with queue groups gives you load balancing and service discovery for free — no service registry, no client-side balancer, no ingress. What you give up is HTTP's tooling: browsers can't speak NATS directly, and your existing observability, gateway, and debugging tools all assume HTTP. Many teams use NATS internally and HTTP at the edge.

How does JetStream compare to a real database?

It doesn't, and shouldn't be asked to. Streams are append-only logs with subject filtering; there are no queries, joins, or secondary indexes. The KV store is genuinely useful for configuration, feature flags, coordination, and leader election — with a watch API that push-invalidates — and it's not a substitute for PostgreSQL. The standard pattern is consuming a stream and projecting into a database you can actually query.

What about multi-region?

This is where NATS is unusually strong. Superclusters connect regional clusters via gateways, and interest propagation means traffic only crosses a region boundary when there's actually a subscriber on the other side. Leaf nodes connect outward from edge locations, so no inbound firewall rules are needed. JetStream mirrors and sourced streams handle cross-region replication of persistent data.

Is it production-ready?

Yes — NATS is a CNCF graduated project, and it's deployed at significant scale, notably as the messaging layer in Synadia's commercial offering and inside numerous platform teams. JetStream is mature, though it's newer than core NATS and its operational characteristics (particularly Raft behavior under partition, and stream limit tuning) are worth understanding before depending on it heavily.

Related Topics

References