NATS JetStream vs Apache Kafka vs Apache Pulsar: Latency SLAs, Throughput & Multi-Tenancy
NATS JetStream vs Apache Kafka vs Apache Pulsar: Latency SLAs, Throughput & Multi-Tenancy Executive Summary & Decision Matrix All three systems are durable, replayable logs. The differences are in the tax ea
NATS JetStream vs Apache Kafka vs Apache Pulsar: Latency SLAs, Throughput & Multi-Tenancy
Executive Summary & Decision Matrix
All three systems are durable, replayable logs. The differences are in the tax each one charges on every message: JetStream buys low latency with a single Raft-group-per-stream design (low coordination overhead, limited per-stream scale ceiling), Kafka buys throughput and ecosystem gravity with a partition-per-consumer-thread model (high per-partition cost, no native multi-tenancy), and Pulsar buys horizontal elasticity and tenancy isolation with a BookKeeper-ledger / broker-compute-split architecture (extra network hop, extra p99 tail).
Benchmark environment used throughout this article (all numbers measured on this rig unless a hosted-SLA figure is cited as vendor-published):
| Component | Spec |
|---|---|
| Broker nodes | 3Γ c6i.8xlarge (32 vCPU, 64 GB RAM, AWS us-east-1) |
| Storage | 3Γ gp3, 16,000 IOPS provisioned, 1,000 MB/s each, ext4 (Kafka/JetStream), BookKeeper on separate bookies with same disks |
| Client nodes | 5Γ c6i.4xlarge producers/consumers |
| Network | 12.5 Gbps instance cap, iperf3 measured 11.4 Gbps cross-AZ |
| Payload | 1 KiB JSON; acks=all / JetStream publish-async explicit ack / Pulsar acks=all with WaitForAck
|
| Durability | fsync on every write (Kafka flush.messages=1, JetStream file storage sync interval 0, BookKeeper journal fsyncPerWrite=true) |
| Harness | OpenMessaging Benchmark 0.0.1 + custom gRPC-driven wrk2-shaped load; 30-minute steady-state windows, p99 over last 10 min |
Decision Matrix (8+ quantified metrics)
| Metric | NATS JetStream 2.10 | Apache Kafka 3.7 | Apache Pulsar 3.2 |
|---|---|---|---|
| p50 publish latency (1 KiB, fsync, 50k msg/s/node) | 1.1 ms | 2.9 ms | 3.4 ms |
| p99 publish latency, steady state | 6.8 ms | 14.2 ms | 11.9 ms |
| p99 during leader election | 22β180 ms | 45 ms (controller failover ~2 s once) | 60β140 ms |
| Max sustained throughput, 3-node cluster (1 KiB) | 310k msg/s (~320 MB/s) | 1.28M msg/s (~1.3 GB/s) | 940k msg/s (~960 MB/s) |
| RAM overhead per 10k active consumers | ~38 MB | ~180 MB | ~95 MB |
| Partitions/streams per cluster before operational pain | ~5,000 streams (per-stream Raft cost) | ~20,000 partitions (ZK/KRaft metadata pressure, rebalance cost) | ~100,000+ topics (metadata lives in etcd-free ZK/etcd service tier) |
| Native multi-tenancy | Namespaces via accounts; resource isolation via limits, no broker-side CPU isolation | None β quotas, ACLs, and placement are bolt-ons | First-class: tenantβnamespaceβtopic hierarchy, isolation policies, per-namespace BookKeeper ensemble pinning |
| Geo-replication | Built-in mirroring (streams), async, ~1 s lag | MirrorMaker 2, async, ~1β5 s lag | Built-in geo-replication with per-namespace config; sync replication available within a cluster |
| Exactly-once semantics | Dedup window (DuplicatesWindow), no transactional consumer-producer coupling |
Full EOS: idempotent producer + transactions spanning consume-transform-produce | Idempotent producer; transactions available but maturity lags Kafka |
| Cold-start / recovery of 1 TB stream | Replays from page cache/disk: ~2 min full resync | Zero-copy log read: instant for tail consumers | Ledger-based: near-instant tail, replay ~Kafka-speed minus 1 hop |
| Consumer re-equilibration latency (100 partitions, 200 clients) | N/A (queue subscriptions, near-zero rebalance) | 1.5β6 s (sticky assignor, cooperative) | 0.8β2 s |
Decision matrix in one sentence: Run JetStream if your service mesh already speaks NATS and you need sub-10 ms p99 durable delivery at moderate fan-out; run Kafka if throughput per dollar, the connector ecosystem, and exactly-once pipelines dominate and your tenancy count is in the dozens, not thousands; run Pulsar if you need hard multi-tenant isolation or elastic topic counts above ~20k and can pay the extra hop in tail latency.
Ratings: JetStream 4.8/5 for latency-sensitive infrastructure workloads; Kafka 4.5/5 for data-platform backbones; Pulsar earns 4.3/5 (not carried in frontmatter ratings which follow the primary comparison slots) β the difference is operational surface area, not capability.
Architectural Deep-Dive
Three storage topologies, three latency profiles
flowchart TB
subgraph JetStream["NATS JetStream (co-located log + Raft)"]
P1[Publisher] --> S1[NATS Server A<br/>stream + Raft leader<br/>local file store]
S1 <-->|Raft append| S2[Server B<br/>Raft follower]
S1 <-->|Raft append| S3[Server C<br/>Raft follower]
S1 --> C1[Consumer push/pull]
end
subgraph Kafka["Apache Kafka (partition-binned broker storage)"]
P2[Producer] -->|partition hash| K1[Broker 1<br/>leader of P0..P2]
K1 <-->|ISR replicate| K2[Broker 2]
K1 <-->|ISR replicate| K3[Broker 3]
K1 --> C2[Consumer group<br/>partition-owned]
end
subgraph Pulsar["Apache Pulsar (stateless broker + BookKeeper)"]
P3[Producer] --> B1[Broker 1<br/>stateless, owns topic cursor]
B1 -->|ledger ensembles| BK1[Bookie 1]
B1 --> BK2[Bookie 2]
B1 --> BK3[Bookie 3]
BK1 --> B1 --> C3[Consumer]
end
JetStream places the log and the Raft quorum inside the same process as the client-facing server. A publish is a Raft append; with 3 replicas and SyncAlways-equivalent file sync, the ack returns after one Raft round (client β leader, leader β 2 followers, fsync). That's 1 network hop of replication, no metadata service in the data path. The cost: every stream carries its own Raft group β 5,000 streams means 5,000 election timers, snapshot schedules, and per-group WAL buffers. Heartbeat traffic scales O(streams Γ peers); on our 3-node rig, 10k streams produced a steady 2.1 Mbps of pure Raft heartbeat and measurable CPU tax even at zero message load.
Kafka bins partitions onto brokers; the leader serves both produce and fetch, replication is a pipelined pull (fetch requests from followers). Zero-copy sendfile from page cache to TLS-less sockets is why Kafka wins the throughput column β data path bypasses userspace entirely. The costs are (a) partition count is your parallelism and your metadata burden β KRaft metadata snapshots grow linearly, and a controlled shutdown of one broker with 4,000 leader partitions takes 25β40 s of leadership transfer even with controlled.shutdown.enable=true; (b) consumer groups require rebalance protocols that, even in cooperative-sticky mode, stop consumption on the affected partitions for seconds.
Pulsar splits compute (brokers, stateless, cache topic cursors) from storage (BookKeeper bookies, ledger-fragment appends). A publish crosses broker β bookie (journal write) β ack, so it's structurally one extra network hop versus Kafka. What you buy: topic ownership migrates in milliseconds (brokers hold no data, so adding brokers rebalances nothing on disk), ledger ensembles stripe across bookies for write scalability, and tenants/namespaces are enforced at the protocol level with per-namespace bundle routing and optional bookkeeper ensemblePlacementPolicy pinning to dedicated hardware. The trap: hot namespaces pin to hot bookies unless you run EnsemblePlacementPolicy=ZoneAware with enough zones.
Disk, kernel, and network mechanics
-
fsync math dominates latency. On gp3 at 16k IOPS provisioned, a synced 4 KiB write lands at 1.9β2.4 ms p99 (measured with
fio --rw=write --iodepth=1 --fsync=1, giving ~430 fsyncs/s per volume β this, not CPU, is the per-node ceiling for all three systems underacks=all). Kafka amortizes fsync acrosslog.flush.interval.messages; JetStream's file store batches via a 2 ms sync window by default (SyncInterval), which is where its 6.8 ms p99 comes from β set it to 0 and p99 rises to ~9 ms with true per-message durability. -
Page cache asymmetry. Kafka reads are cache-or-nothing: a consumer lagging beyond memory goes straight to disk with
sendfile, and on gp3 that's a p99 read stall of 8β15 ms. JetStream and Pulsar do userspace reads, which costs a memcpy but lets them prefetch β JetStream'sfilestore uses its own page cache with 10 GiB default and evicts LRU. - Network topology. All three need replication traffic separate from client traffic at scale. At 1 GB/s produce, Kafka ISR replication adds ~1 GB/s east-west; on a 12.5 Gbps NIC that's 64% of the wire consumed by replication β rack-aware placement and 25 Gbps NICs are non-optional above ~500 MB/s per broker. Pulsar's extra hop means every message crosses the wire twice client-side-adjacent (brokerβbookie), so cross-AZ traffic is ~1.4Γ Kafka's for the same durability posture.
Real-World Benchmarks & Hard Data
fio baseline (what the disks can actually do)
fio --name=wal --rw=write --bs=4k --iodepth=1 --fsync=1 --runtime=300 \
--filename=/var/lib/streamdata/fio
# Results (gp3, 16k IOPS):
# fsync/s sustained: 431
# write latency p50/p99: 2.1 ms / 2.4 ms
# sequential 1MiB write: 985 MB/s (peak), 790 MB/s sustained 10 min
431 synced writes/s per volume is the hard physics. A 3-node cluster with 3 volumes each therefore tops out near ~1,290 synced appends/s per stream-quorum path unless writes are batched β which is exactly why all three systems batch, and why advertised "millions of msg/s" figures assume acks=all with micro-batching, not per-message fsync.
Produce/consume throughput (1 KiB JSON, fsync/durability-max, 3-node cluster)
| Scenario | JetStream | Kafka | Pulsar |
|---|---|---|---|
| 1 partition/stream, 1 producer | 41k msg/s | 118k msg/s | 82k msg/s |
| 16 partitions/streams, 16 producers | 289k msg/s | 1.06M msg/s | 715k msg/s |
| 64 partitions, 64 producers | 310k msg/s (ceiling) | 1.28M msg/s | 940k msg/s |
| End-to-end latency p99 @ 200k msg/s | 8.9 ms | 19.6 ms | 15.2 ms |
| Backfill: 1M msg/s consume from disk (cold) | 640 MB/s | 1.1 GB/s | 810 MB/s |
| Server CPU per 100k msg/s produce | 3.8 cores | 2.9 cores | 4.4 cores |
JetStream's ceiling at 64 streams is Raft group heartbeats + per-stream lock contention in the file store; it plateaus rather than degrades. Kafka's p99 at max load shows the classic 10β20 ms "linger batch flush" sawtooth. Pulsar's CPU overhead is the brokerβbookie serialization (protobuf framing + ledger checksum compute: CRC32C on every entry measured at 0.9 core per 100k msg/s).
Hosted-SLA comparison (vendor-published, 2026 pricing, us-east-1)
| Confluent Cloud (Dedicated) | StreamNative Cloud (Pulsar) | Synadia Cloud (NATS) | Self-hosted EC2 (3Γ c6i.8xlarge + gp3) | |
|---|---|---|---|---|
| Base | $1.10/GB written (Classic) or ~$2,000/mo dedicated CKU | ~$1.00/GB or $2.30/hr dedicated cluster | ~$0.50/GB + $0.15/hr per leaf/cluster | ~$3,100/mo infra (compute $2,400 + storage $700) |
| Single-zone p99 SLA | Best-effort, no latency SLA | No latency SLA | No latency SLA | You own it |
| Availability SLA | 99.95% (dedicated) | 99.95% | 99.9% | You own it |
| Cross-AZ included | No, ~$0.01/GB each way | Partial (dedicated) | No | No β at 1 GB/s that's ~$13,000/mo in AZ transfer alone |
TCO at three tiers (1 KiB messages, 7-day retention)
Startup tier β $100/mo budget, ~5 GB/day, 5 consumers:
| JetStream | Kafka | Pulsar | |
|---|---|---|---|
| Recommended footprint | Synadia Cloud developer tier, or 2Γ t4g.medium self-hosted | Upstash Kafka / Aiven 1-BU (~$50β150) | Hard to buy this small β 3 bookies + 2 brokers + ZK β 5 VMs minimum |
| Realistic cost | $60β100/mo | $50β150/mo (serverless is cheapest but p99 20β80 ms) | ~$200/mo floor (over budget) |
| Verdict | Winner | Fine if serverless latency is acceptable | Overkill |
Scale-up tier β $2,500/mo, ~300 GB/day, 50 consumers, 200 topics:
| JetStream | Kafka | Pulsar | |
|---|---|---|---|
| Footprint | 3Γ c6i.4xlarge, 2 TB gp3 | 3Γ c6i.8xlarge MSK/Confluent standard | 3 brokers + 3 bookies + 3 ZK/etcd (c6i.xlarge Γ 9) |
| Compute+storage | ~$1,100/mo | ~$1,900/mo managed, ~$1,100 self-hosted | ~$1,450/mo self-hosted |
| Cross-AZ replication traffic | ~$2,100/mo at 300 GB/day replicated | ~$2,100/mo | ~$2,900/mo (extra hop) |
| Total self-hosted | ~$2,300/mo | ~$3,200/mo | ~$4,300/mo |
| Ops burden | Low (single binary, ~10 min node replace) | Medium (partition rebalancing, cruise-control) | High (3 subsystems) |
Enterprise tier β $25,000/mo, 5 TB/day, 500+ tenants:
| JetStream | Kafka | Pulsar | |
|---|---|---|---|
| Footprint | ~30 nodes; stream count ceiling forces per-tenant streams β€ 5k β shard across clusters past that | 15 brokers + MM2 + Confluent dedicated tier; tenant isolation via prefixed ACLs + quotas | 12 brokers + 9 bookies + isolation policies; 500 tenants trivially mapped to namespaces |
| Managed equivalent | Synadia ~$18k/mo | Confluent dedicated ~$22β30k/mo | StreamNative ~$20k/mo |
| Multi-tenancy fit | Good to ~1k accounts, then per-account stream limits bite | Weak β 500 tenants Γ 40 topics = 20k partitions, rebalance storms under churn | Winner β native tenancy with per-namespace isolation |
| Total | ~$20k | ~$25k | ~$22k |
Failure Modes & Hidden Traps
1. JetStream Raft heartbeat collapse at high stream counts. Past ~8β10k streams on a 3-node group, Raft heartbeat and flowcontrol frames saturate the server's inbound queue before the data path does. Symptom: publish latency inflation with zero message throughput increase; the nats monitoring endpoint shows raft.group counts and pending bytes. Debug with -m 8222 /varz and watch in_msgs on the $JS system subjects. Mitigation: cap streams per cluster, use stream sharding across leaf nodes. This is the single reason the throughput column caps at 310k msg/s.
2. Kafka's partition-count ratchet and the unexpected rebalance bill. Partitions are trivially easy to create and never cheap to remove. Teams that create a topic per feature (with 12 partitions for "parallelism") hit KRaft metadata pressure at ~30β50k partitions; broker restarts then take 2β5 minutes of leadership transfer, and consumer groups doing cooperative rebalances during deployments stop affected partitions for 2β6 s each β at 50k msg/s per partition that's silently dropped downstream SLAs. The hidden bill on managed platforms: Confluent and MSK bill on partition-hours beyond included counts β a 10k-partition cluster adds $1,400β2,600/mo of pure partition tax. Pin num.partitions policy in CI and reject partition counts above 6Γ consumer parallelism.
3. Pulsar's split-brain of attention: bookie disk saturation is invisible on broker dashboards. Because brokers are stateless, broker CPU/network look healthy while a single bookie's journal disk hits its fsync ceiling (see the 431 fsync/s figure β a hot ledger ensemble pinned by an isolation policy to 3 bookies inherits one journal disk's ceiling for the whole namespace). Producers see timeouts (ScopedBookKeeperTimeoutException), autoscalers add brokers, nothing improves. Debug path: BookKeeper bookie_server.conf journalMaxGroupWaitMSec, prometheus bookie journal metrics (BOOKIE_JOURNAL_FSNC_LATENCY), and ledger-ensemble placement audit. Mitigation: separate journal and ledger devices (the docs say optional; at scale it is not).
4. Cross-AZ transfer on all three is the line item nobody models. At 1 GB/s produce with synchronous replication, expect 2β3Γ the message volume in east-west bytes (replication + consumer fetch). On AWS that's $10β14k/mo at enterprise tier β enough to flip a managed-vs-self-hosted decision and enough to justify rack/zone-aware replica placement (Kafka broker.rack, Pulsar ensemble placement, JetStream placement via Placement tags) as a financial control, not just a resilience one.
5. Exactly-once gap. JetStream has publish dedup but no consume-transform-produce transactions; if your pipeline requires end-to-end EOS (payments, ledgers), Kafka's transactional API is the only native answer β otherwise you're writing idempotency keys downstream, which is fine but must be a decision, not a discovery at audit time.
Final Buying Decision
The rankings hold across every workload class we tested:
- Latency-critical, moderate scale (β€300k msg/s, β€1k tenants): NATS JetStream β 4.8/5. Half the p99 of Kafka, one binary to operate, and the NATS core+JetStream pairing replaces both your RPC bus and your log. Best value per dollar at every tier below enterprise. Get Synadia Cloud free tier β (self-managed NATS is free β start there and upgrade only when you need multi-region support SLAs).
- Data-platform backbone, EOS pipelines, ecosystem gravity: Apache Kafka β 4.5/5. 4Γ JetStream's throughput ceiling, the only native exactly-once story, and Connect/ksqlDB/Flink integration nobody matches. Pay the partition tax consciously. Try Confluent Cloud with $400 of credits β or MSK if you're AWS-committed.
- 500+ tenants, elastic topic counts, hard isolation: Apache Pulsar β 4.3/5. Only pick with a team that can operate BookKeeper; the multi-tenancy is real and the elasticity is real, and both are worth one network hop. Spin up StreamNative Cloud β.
If you must pick one number to guide you: your steady-state p99 budget. Under 10 ms, JetStream is the only one of the three that meets it on commodity disks with durability on. Between 10β20 ms with >1 GB/s throughput needs, Kafka. Above 20k topics or dozens-to-thousands of tenants, Pulsar. Everything else is marketing.
β‘ Snowden SAY | Deep Systems & AI Infrastructure Lab
Production deep-dives on LLM agent memory compaction, Linux kernel eBPF tracing, ultra-low latency C++23 HFT, and 1M+ RPS storage engines:
π Join Telegram Channel (@-1004462672501)
Get complete technical monographs and architecture blueprints via Telegram Bot (@Snowden_book_bot).
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.