Skip to Content
Comparisonsvs NATS JetStream

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 subjectsorders.*.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

topicsNATS JetStream
AddressingWrite to a topic by nameStream binds subjects (orders.>) and captures matching publishes
Producer couplingProducer names the topicProducer publishes to a subject; the stream captures it
Fan-outServer-side routers copy topic→topicMultiple streams can bind overlapping subjects; consumers filter by subject
WildcardsTag prefix match on reads/deletes; topic names are literalFirst-class subject wildcards (*, >)
TransportPlain HTTP/JSON + SSE — curl is a clientNATS 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 streamfile 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 as disk, 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 group fdatasync completes, 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.
  • AckWait is a redelivery deadline — a lease: if you don’t ack within the window, the message is redelivered. BackOff lets 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_seq cursor. Advancing it is the ack. The server tracks no per-consumer position — it’s stateless on the read path, which is what lets curl be a complete client.
  • Lease queues (queues): claim hands out a lease with a deadline (lease_ms, default 30 000 ms); ack is the permanent delete; nack requeues; extend renews the lease. This is the closest analog to AckWait redelivery, 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 MaxDeliver advisory you have to consume and route yourself.
  • You want plain HTTP/JSON + SSEcurl is 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/5 with 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/BackOff redelivery semantics.
  • You need hard account isolation and native TLS (plus WebSocket/MQTT), not a prefix-allowlist filter behind a proxy.

See also

Last updated on