vs NATS JetStream
NATS JetStream is the fairest peer to topics: both are persistence layers that live inside a single server binary, write to local files, and serve as the substrate for log, queue, and fan-out patterns. The honest split is that JetStream also clusters for replicated HA and scale-out, while topics is single-machine; and that topics gives you a delivery-time gap signal and a built-in dead-letter move, which JetStream leaves to advisories you wire up yourself.
This page describes JetStream (the persistence/streaming layer inside nats-server),
not core NATS subject messaging. JetStream can run as one binary — that’s why it’s the
closest comparison — but it is also designed to run as a RAFT-replicated cluster, which
topics is not.
The shape of each system
topics writes records to named topics addressed directly by name (orders,
audit). A topic has a per-topic durability class and is read by
its monotonic seq cursor over plain HTTP/JSON, with live delivery over
multiplexed SSE.
JetStream defines streams that bind one or more subjects —
orders.*.created, orders.> — and capture every message published to a matching
subject, decoupled from any particular publisher. You interact through the NATS protocol
and a NATS client library; consumers are objects the server tracks on your behalf.
That subject-binding model is genuinely powerful and has no equivalent in topics: a JetStream stream pulls in traffic by wildcard pattern without the producer naming the stream. In topics you write to a topic by name, and routers handle fan-out after the fact.
Subjects vs topics
| topics | NATS JetStream | |
|---|---|---|
| Addressing | Write to a topic by name | Stream binds subjects (orders.>) and captures matching publishes |
| Producer coupling | Producer names the topic | Producer publishes to a subject; the stream captures it |
| Fan-out | Server-side routers copy topic→topic | Multiple streams can bind overlapping subjects; consumers filter by subject |
| Wildcards | Tag prefix match on reads/deletes; topic names are literal | First-class subject wildcards (*, >) |
| Transport | Plain HTTP/JSON + SSE — curl is a client | NATS wire protocol + a NATS client library |
Subject wildcard capture is a clear JetStream win. The topics win is reach: any HTTP client, no protocol or SDK, and SSE for live tailing.
Per-stream vs per-topic durability
JetStream sets storage per stream — file or memory. That is a binary choice for
the whole stream.
topics resolves a durability class per topic, with four levels rather than two:
ephemeral— resident-only records: queryable while the process runs, durable config, monotonic seqs, and intentionally empty after restart.memory— disk-like but best-effort: same group-committed WAL path asdisk, no durability guarantee (records may survive or be lost on restart; config always persists).disk— WAL + group commit, acked on frame enqueue, survives a crash minus the un-fsynced tail.fsync— ack is held until the groupfdatasynccompletes, so an acknowledged write survives any crash.
A RAM-only live feed, a best-effort cache topic, a group-committed feed, and an
fsync-gated ledger topic coexist in one process, each paying only for the guarantee it
needs. JetStream’s file storage is closer to topics’ disk/fsync end; the per-topic
granularity and the explicit disk middle tier are a topics distinction.
Consumers + AckWait vs lease queues
JetStream consumers are server-tracked. The server remembers each consumer’s position and acknowledgements:
- Ack policies
AckNone,AckAll,AckExplicit. AckWaitis a redelivery deadline — a lease: if you don’t ack within the window, the message is redelivered.BackOfflets you schedule escalating redelivery intervals.- Push and pull consumers, durable or ephemeral.
These are mature, well-worn ack semantics, and the server holding consumer state is a real convenience.
topics splits this into two explicit models:
- Cursor reads (diff): the consumer owns its
from_seqcursor. Advancing it is the ack. The server tracks no per-consumer position — it’s stateless on the read path, which is what letscurlbe a complete client. - Lease queues (queues):
claimhands out a lease with a deadline (lease_ms, default 30 000 ms);ackis the permanent delete;nackrequeues;extendrenews the lease. This is the closest analog toAckWaitredelivery, and it is production-ready today.
JetStream’s win here is server-side consumer bookkeeping and BackOff scheduling out of
the topic. The topics win is that the cursor model needs no server-side consumer object at
all, and the queue model gives you per-message leases with the same redelivery guarantee.
MaxDeliver advisory vs built-in dead-letter MOVE
This is a concrete behavioral difference.
In JetStream, MaxDeliver caps redelivery attempts. When a message exhausts them,
JetStream publishes an advisory ($JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.…).
There is no built-in destination — you subscribe to the advisory and build your own
dead-letter handling.
topics moves the message for you. When a queue job is about to be delivered for the
(max_deliveries + 1)-th time and the topic has a dead_letter target, the job is
moved into the dead-letter topic — stamped with meta.$dead_letter_from,
$dead_letter_deliveries, and $dead_letter_src_seq — and deleted from the source
queue. No advisory consumer to operate; the poison message lands in a topic you can inspect
and replay.
The dead-letter copy honors the destination topic’s durability class. Point
dead_letter at an fsync topic if you need poison messages to survive a crash.
Gap detection
JetStream — like every system on these comparison pages — has no in-band gap signal.
When retention (Limits / DiscardOld) evicts messages a consumer hasn’t reached, the
consumer infers the loss from a jump in the stream sequence number. There is no
delivery-time “you missed X..Y” frame.
topics makes involuntary loss explicit. If cap eviction or TTL expiry removed records
below a consumer’s cursor, the read returns an in-band tombstone
with the exact [gap_from, gap_to] range — at HTTP 200, on the same response that
carries the surviving records. A purely deleted gap reads silently (you asked for that
removal); only involuntary loss tombstones. That distinction is the
load-bearing invariant of the whole data model.
topics also lets a topic reject instead of evicting: with discard:"reject", a full
topic fails the producer with 422 topic_full before a seq is assigned, rather than
dropping the oldest record. JetStream’s DiscardNew similarly refuses new messages on a
full stream; the difference is that topics pairs rejection-on-write with a tombstone
on the read side when eviction does happen.
KV / Object store
JetStream ships KV and Object stores built on top of streams-as-subjects — a genuine convenience for config, sessions, and large blobs without bolting on another system.
topics has no KV or Object abstraction. A topic is an append-only log; the closest thing to “latest value for a key” is a tag plus a point-in-time delete, which is not the same as a KV bucket. If you want a key/value or object API in the same binary, JetStream wins outright.
RAFT replication / HA
This is the central honest difference.
JetStream streams can be RAFT-replicated with replicas: 1 | 3 | 5. A 3- or 5-replica
stream survives the loss of a node, with automatic leader election and failover — real
high availability.
topics has no replication, no failover, no HA. A single machine is the entire
durability and failure domain. An fsync topic survives any crash of that machine and
replays its WAL on restart, but if the machine or its disk is gone, the data is gone
until you restore a backup. There is no quorum, no follower, no automatic failover.
If you need to survive the loss of a node without operator intervention, you need replication — and that is JetStream (or another replicated system), not topics. topics trades HA for the simplicity of one process and one disk.
Accounts isolation + native TLS, and scale
JetStream has first-class accounts — hard multi-tenant isolation, where subjects and
streams in one account are invisible to another. Auth is nkeys/JWT, and the server
terminates TLS natively (plus WebSocket and MQTT gateways). For scale, NATS forms
clusters, superclusters, and leaf nodes that span regions.
topics is more modest on all three axes, and says so:
- Isolation is a prefix allowlist per API key — a filter, not a partition. Two keys can be scoped to non-overlapping topic-name prefixes, but this is not hard tenant isolation, and a hard namespace partition is out of scope (not a planned feature).
- TLS is not terminated by topics. Run it behind a TLS-terminating reverse proxy
for any non-loopback exposure. Auth is bearer keys, hashed SHA-256 at rest and compared
in constant time, with per-key scopes (
read/write/delete/admin). - Scale is vertical only — one NVMe-backed machine. The engine core sustains millions of records/sec in-process and a single HTTP origin tops out around 0.5 M rec/sec disk-class, but you scale up, not out. See Performance.
Verdict
topics wins when
- You want a delivery-time gap signal — an explicit tombstone
with the exact
[gap_from, gap_to]range — not a silent stream-sequence jump to reverse-engineer. - You want a built-in dead-letter move for poison messages, not
a
MaxDeliveradvisory you have to consume and route yourself. - You want plain HTTP/JSON + SSE —
curlis the client — with no NATS protocol or client library. - You want per-topic durability classes (ephemeral / memory / disk / fsync) rather than one per-stream file/memory switch.
- You want a producer to fail fast (
discard:“reject”,422 topic_full) before a seq is assigned on a full topic. - You want one self-contained server and one disk, with no cluster to operate.
NATS JetStream wins when
- You need replicated HA — RAFT
replicas: 3/5with automatic leader election and failover. - You need scale-out across clusters, superclusters, or region-spanning leaf nodes.
- You want subject wildcard capture (
orders.>) that decouples streams from producers. - You want a built-in KV or Object store in the same binary.
- You want server-tracked consumers with mature
AckWait/BackOffredelivery semantics. - You need hard account isolation and native TLS (plus WebSocket/MQTT), not a prefix-allowlist filter behind a proxy.
See also
- Comparisons overview — the full capability matrix across all four systems.
- vs Apache Kafka — the distributed-cluster end of the spectrum.
- Tombstones — the explicit gap signal in depth.
- Queues API — lease, ack, nack, extend, and dead-letter.