NATS JetStream vs Kafka for Teams That Do Not Need Kafka's Scale

Photo by D Gore via Wikimedia Commons (CC BY-SA 2.0)
For the majority of backends, yes. JetStream provides durable, replayable streams, push and pull consumers, and at-least-once or exactly-once delivery. It is not a replacement when you specifically need Kafka Connect's connector ecosystem, sustained multi-million message-per-second throughput, or petabyte-scale long-term retention.
Publishers attach a unique message ID that the server uses to deduplicate messages within a configurable window, preventing duplicate publishes. On the consumer side, a double-acknowledgment confirms the message was both received and processed. Together these give exactly-once semantics without the transaction coordination Kafka requires.
Push consumers have the server deliver messages automatically to a subject, which is simple but offers less flow control. Pull consumers let the client request batches when ready, giving natural backpressure and horizontal scaling. Pull consumers are the recommended default for most durable processing workloads.
No. Kafka 4.0, released in March 2025, removed ZooKeeper entirely in favor of KRaft, its own Raft-based metadata layer. This simplifies the architecture to a single system and scales to millions of partitions, but it does not remove partition planning or consumer group tuning.
JetStream is built into the single nats-server Go binary and is enabled with one configuration block or flag. There is no JVM to tune, no separate ZooKeeper or KRaft controller, and monitoring is a built-in HTTP endpoint. A three-node clustered deployment runs comfortably on modest VPS resources.

Photo by D Gore via Wikimedia Commons (CC BY-SA 2.0)
Key Takeaway
For teams processing thousands of messages per second rather than millions, NATS JetStream delivers durable streams, push and pull consumers, and at-least-once or exactly-once delivery from a single self-contained binary. Kafka wins at massive throughput and long retention, but its partition planning and operational surface are overkill for most backends.
Every few months a team reaches for Kafka because it is the default answer to "we need a message log." Then they spend a sprint on partition counts, consumer group rebalancing, retention tuning, and wiring up an observability stack before a single business event flows through it. I have watched this happen on services that peak at a few thousand messages per second. That is not Kafka's home turf, and paying Kafka's operational tax to sit in it is a bad trade.
NATS JetStream is the persistence layer built into the NATS server. If you already speak the NATS subject model, you get durable streams, replayable history, and delivery guarantees by flipping one configuration flag. This post walks through streams, consumers, and the operational reality of running each, with a comparison table so you can make the call for your own workload.
Core NATS is fire-and-forget pub/sub: if no subscriber is listening, the message is gone. JetStream adds a storage layer on top of the same server, so messages published to a subject are captured into a stream and can be replayed on demand. There is no separate process, no separate cluster, and no separate client library. You enable it with one flag and keep the same connection and subject model you already use.
# Enable JetStream in the server config file
# nats-server.conf
jetstream {
store_dir: "/data/jetstream"
max_memory_store: 1G
max_file_store: 10G
}
# Or just a flag for local dev
nats-server -jsA stream defines what subjects it captures and how it retains data. The three retention policies matter because they map directly to common backend patterns.
A consumer is a stateful view into a stream that tracks which messages a client has seen. Pull consumers are the modern default: the client asks for a batch when it is ready, which gives you natural backpressure and horizontal scaling without ever defining a partition. Multiple workers can share one pull consumer like a queue group, and the server load-balances across them.
Acknowledgment policy is where you tune delivery guarantees. Explicit ack means each message must be confirmed or it will be redelivered after a timeout — this is your at-least-once guarantee. JetStream also offers exactly-once using publisher message IDs for deduplication plus a double-ack on consumption. Contrast that with Kafka, where exactly-once requires transactions and idempotent producers configured correctly across the pipeline.
// TypeScript, using the nats.js client with JetStream
import { connect, AckPolicy } from "nats";
const nc = await connect({ servers: "localhost:4222" });
const jsm = await nc.jetstreamManager();
// Create a durable, file-backed stream
await jsm.streams.add({
name: "ORDERS",
subjects: ["orders.>"],
retention: "limits",
storage: "file",
num_replicas: 3,
});
// Create a durable pull consumer with explicit ack
await jsm.consumers.add("ORDERS", {
durable_name: "order-workers",
ack_policy: AckPolicy.Explicit,
max_deliver: 5,
ack_wait: 30_000_000_000, // 30s in nanoseconds
});
const js = nc.jetstream();
const consumer = await js.consumers.get("ORDERS", "order-workers");
const messages = await consumer.consume({ max_messages: 100 });
for await (const m of messages) {
await handleOrder(m.data);
m.ack();
}Set max_deliver to a finite number and pair the stream with a dead-letter strategy. Without a delivery cap, a message your handler always throws on will be redelivered forever, quietly burning a worker in a loop. This is the JetStream equivalent of a Kafka poison-pill and it bites teams that assume redelivery is free.
Kafka 4.0, released in March 2025, removed ZooKeeper entirely in favor of KRaft, its own Raft-based metadata layer. That is a genuine simplification — one system instead of two, and clusters that scale to millions of partitions. But KRaft does not remove the parts that actually cost you time: partition planning, consumer group rebalancing, retention and segment tuning, and understanding distributed log semantics before you can debug a lag spike.
JetStream ships as a single Go binary with no JVM to tune and no heap to size. On the VPS I run, a three-node clustered NATS deployment with JetStream enabled fits comfortably in memory that a single Kafka broker would consider an insult. Replication is Raft-based per stream, monitoring is a built-in HTTP endpoint, and the same nats CLI you use to publish also inspects streams and consumers. The whole thing is one mental model.
Do not read "lightweight" as "unlimited." JetStream is not built for petabyte-scale retention or the vast third-party connector ecosystem Kafka Connect gives you. If you need to fan out into dozens of data warehouses and lakes, or you truly are pushing millions of messages per second with month-long retention, Kafka earns its complexity. Match the tool to the actual load, not the load you imagine.
| Dimension | NATS JetStream | Apache Kafka 4.0 |
|---|---|---|
| Deployment | Single Go binary, one flag to enable, no JVM | JVM-based brokers, KRaft controllers, memory sizing |
| Scaling unit | Consumers over subjects, no partition planning | Partitions per topic, planned up front |
| Delivery guarantees | At-least-once by default, exactly-once via msg IDs plus double-ack | At-least-once, exactly-once via transactions and idempotent producers |
| Retention models | Limits, Work Queue, Interest — per stream | Time or size based log retention, plus compaction |
| Peak throughput | Comfortable at thousands to low millions msg/s | Purpose-built for sustained millions of msg/s |
| Ecosystem | Lean, subject-based, edge and multi-tenant friendly | Vast — Kafka Connect, Streams, Schema Registry |
My rule is simple: reach for Kafka when you can name the specific Kafka feature you need — Connect, sustained massive throughput, or long compacted history. If your honest answer is "I need a durable, replayable message log with good delivery guarantees," JetStream gives you that with a fraction of the operational surface, and you can always graduate later if the load truly arrives.