NATS JetStream vs Kafka for Edge and IIoT Telemetry: A 2026 Architecture Decision Record
The default answer to “where do we put the telemetry?” has been Apache Kafka for the better part of a decade, and for a data centre that default is usually right. But the moment your topology stops being one fat cluster in one region and becomes two hundred factory floors on flaky cellular links, the default starts to fight you. This is an architecture decision record about that exact tension, and the framing that makes it tractable is NATS JetStream vs Kafka as two fundamentally different bets on where durability and topology should live. One was designed as a distributed commit log for high-throughput data-centre pipelines. The other was designed as a connective nervous system that happens to have grown a persistence layer. At the edge, that lineage decides almost everything.
I am going to keep NATS JetStream as the protagonist here, because it fills a gap that Kafka genuinely struggles with — thousands of small sites, intermittent WAN, tiny compute budgets — but I am going to give Kafka a fair and accurate hearing, because for the central lakehouse it remains the stronger tool. This is a systems analysis grounded in how each broker actually behaves, not a vendor endorsement of either project.
What this covers: the edge/IIoT telemetry problem, the core architectural distinction between the two brokers, a mechanism-level look at delivery guarantees and back-pressure, a decision matrix, the failure modes that bite teams in production, and a concrete recommendation with a checklist.
Context and Background
Industrial IoT telemetry has a shape that most streaming tutorials quietly ignore. You do not have one producer and one broker; you have hundreds or thousands of geographically dispersed sites — plants, substations, wind farms, retail back-rooms, moving vehicles — each generating a modest but relentless trickle of sensor, PLC, and machine-health data. The compute at each site is small: an industrial gateway, a fanless box, sometimes an ARM SBC pulling ten watts. The network between site and cloud is the villain of the story. It is a cellular modem that drops for ninety seconds when a truck reverses past the antenna, a satellite uplink with 700 ms of latency, or a plant firewall that only opens outbound 443. Engineers describe these as DDIL conditions — disconnected, degraded, intermittent, and limited — and any telemetry design that assumes a healthy link will lose data the first week it meets a real one.
The usual “just use Kafka” answer struggles here for reasons that are structural, not incidental. A Kafka producer at the edge is a client, not a store; if the link to the brokers is down, your local buffering is whatever the producer’s buffer.memory and linger.ms allow before it blocks or drops. Running a full Kafka broker at each site to get local durability means shipping a JVM that wants a 6 GB heap minimum and, with page cache, 10–12 GB of memory per broker — multiplied by however many sites you have, plus the operational burden of patching, sizing, and monitoring all of them. That is the crux of the edge telemetry message broker problem: durability has to live where the data is born, but the traditional log broker is too heavy to put there.
This is why the comparison is worth doing carefully rather than by reputation. If you are building the field side of an IIoT stack, you are probably already thinking about protocol translation at the gateway — our MQTT Sparkplug B reference architecture covers that layer — and the message broker sitting behind it determines whether the whole system degrades gracefully or falls over. NATS was, per its own project overview, built as cloud-native messaging with edge reach as a first-class goal, which is exactly the property the naive Kafka deployment lacks. The rest of this ADR is about how much that matters, where it stops mattering, and what the honest trade-offs are.
NATS JetStream vs Kafka: the core distinction
Here is the direct answer before the detail: choose NATS JetStream when your hard problem is topology — many small sites, unreliable links, tiny footprint — and choose Kafka when your hard problem is a high-throughput central pipeline with a mature processing and connector ecosystem. JetStream pushes durability out to the edge cheaply; Kafka concentrates it in a cluster brilliantly. Most serious IIoT platforms end up using both, with a bridge between them, and the interesting decision is where you draw that line.

To make the distinction concrete, look at what each system treats as its fundamental unit. That difference cascades into the data model, the delivery guarantees, and the operational footprint, so I will take those three in turn.
Data model: subjects vs partitioned topics
NATS is built on subjects — lightweight, hierarchical, dot-delimited address strings like factory.berlin.line3.press.temp. Subjects are ephemeral routing tokens by default; publishing to one is a fire-and-forget operation against a message-routing fabric. Wildcards are first-class: * matches a single token and > matches one or more trailing tokens, so a consumer can subscribe to factory.berlin.> and receive everything from that site, or factory.*.line3.press.temp to watch one press across every plant. Per the NATS subjects documentation, this hierarchy is the primary organising principle of the whole system, and it maps almost perfectly onto IIoT’s naturally hierarchical namespace of site, cell, machine, and signal.
JetStream then layers persistence on top. A stream is a server-side store that captures messages published to a set of subjects; a consumer is a stateful, server-tracked view over that stream. Crucially, the subject namespace and the storage are decoupled — one stream can capture thousands of subjects, and you can add or reshape consumers without touching the producers. NATS 2.10 added FilterSubjects, letting a single consumer bind an array of subject filters, which is how you carve arbitrary slices out of a firehose without re-partitioning anything.
Kafka’s fundamental unit is the partition: an append-only, totally ordered log segment. A topic is a named collection of partitions, and the partition count is the unit of both parallelism and ordering. Producers hash a key to a partition; consumers in a consumer group each own a subset of partitions. This is a beautiful model for throughput — partitions are how Kafka scales linearly to millions of messages per second — but it is a rigid one for a sprawling namespace. You do not get wildcard subscription across a device hierarchy; you get topics and partition keys, and repartitioning a live topic to change parallelism is a genuine operation. For a data-centre pipeline that rigidity is a feature. For a device tree that grows a new signal type every quarter, it is friction.
Delivery and ordering guarantees
Core NATS gives at-most-once delivery — fast, fire-and-forget, no persistence. JetStream upgrades this to at-least-once, and, within a bounded window, exactly-once publish. The mechanism matters. On publish you can set a Nats-Msg-Id header; the stream rejects any duplicate carrying the same ID inside its deduplication window (a configurable per-stream interval, two minutes by default), per the JetStream model deep-dive. On the consume side, acknowledgement policies range from none through all (acking one message implicitly acks all prior) to explicit (every message acked individually). An un-acked message is redelivered after an AckWait timeout. Order within a stream follows stream sequence, and a consumer sees messages in that order for a single subject filter.
The honest caveat — and I will return to it in the failure-modes section — is that “exactly-once” here means exactly-once publish into the stream inside the dedup window. It does not make end-to-end processing exactly-once, because no distributed system can promise that. What you actually build is effectively-once: at-least-once delivery plus idempotent consumers. Anyone who tells you JetStream gives you free exactly-once semantics end to end has skipped the fine print.

Kafka’s ordering guarantee is stronger in one specific dimension: total order within a partition, always, by construction. Its exactly-once story is also more mature for stream processing — the transactional producer plus idempotence, combined with Kafka Streams or Flink, gives you read-process-write exactly-once across the pipeline, which JetStream does not natively match. If your central analytics genuinely require transactional exactly-once across many topics, that is a real Kafka advantage. The trade is that all of this ordering and transactionality is anchored to the partition, which lives in the cluster — so you get the guarantee where Kafka runs, which is precisely not at a disconnected edge.
Footprint and operations
This is where the NATS JetStream vs Kafka comparison stops being philosophical and starts being about your on-call rotation. A NATS server is a single, statically compiled Go binary of roughly 15–25 MB with no external dependencies, idling at single-digit megabytes of RAM and rising to a few hundred megabytes with JetStream streams active. A leaf node — a full NATS server running at the edge that transparently bridges into a central cluster — fits comfortably on the ten-watt gateway you already have. Benchmarks consistently put NATS at the light end: one comparison measured it at about 6 MiB on cold start versus Kafka’s much larger baseline, a roughly two-orders-of-magnitude difference before JetStream is even loaded.
Kafka, post-4.0, is operationally lighter than it was — ZooKeeper is gone, replaced by the built-in KRaft consensus protocol, which cut a whole distributed system out of the deployment. That is a real and welcome simplification, and if you are running Kafka centrally you should be on KRaft; our KRaft production migration tutorial walks through it. But “lighter Kafka” is still a JVM cluster that wants gigabytes of heap and page cache per broker, quorum controllers, careful anti-affinity, and partition-count planning. It is a fine thing to run three of centrally. It is a miserable thing to run two hundred of at the edge. NATS is simply built for the second shape and Kafka is not.
Deeper analysis: edge topology, delivery guarantees, back-pressure, cost
Footprint is the headline, but topology is where the two systems diverge most usefully for IIoT, so let me put the mechanism under a microscope. NATS models the edge with leaf nodes and superclusters. A leaf node is a full server that runs locally, accepts local publishers and subscribers at LAN latency, persists to its own JetStream store, and maintains a single outbound connection to a regional hub or supercluster. When the WAN is healthy, messages flow up and durable state syncs. When the WAN drops, the leaf keeps accepting and storing local traffic; when it reconnects, it resumes. This is store-and-forward as a native property of the fabric rather than something you bolt on. Per the NATS adaptive edge deployment guide, you can evolve this topology — add regions, sites, or cloud backends — without redesign or downtime, because the connection model is uniform all the way down.

Kafka’s answer to geography is different in kind. There is no lightweight edge broker in the box; you connect edge producers directly to central brokers (fragile over DDIL links), replicate between full clusters with MirrorMaker 2 or a commercial replicator (heavy for hundreds of sites), or run a stretch cluster (which hates high-latency links because it is quorum-sensitive). Kafka’s genuine strength lives in the cluster: with tiered storage offloading old segments to object storage, a central Kafka can retain months of history without sizing broker disk to match, and its throughput ceiling — comfortably into seven figures of messages per second — outclasses what a JetStream cluster will do at the same hardware spend. So the shape that emerges is not “one or the other” but a division of labour: JetStream owns the topology-hard edge, Kafka owns the throughput-hard core.
Back-pressure is the mechanism that separates a design that survives a bad day from one that amplifies it. JetStream’s pull consumers give the application explicit flow control: the consumer requests a batch and acknowledges as it processes, so a slow analytics job simply pulls slower and the stream buffers to its configured limits rather than overrunning anyone. Stream limits — MaxBytes, MaxMsgs, MaxAge — plus a discard policy define exactly what happens when the store fills: reject new writes or evict old ones. That is bounded, predictable degradation, which at the edge is worth more than raw speed. Kafka handles back-pressure through consumer lag against a retention window: producers keep appending, consumers fall behind, and as long as retention outlasts the lag you lose nothing. It works well when disk (or tiered storage) is plentiful and the link is stable — again, the data-centre assumption.
Cost tracks these mechanisms directly. The dominant line item at the edge is not licensing but the fleet: memory, CPU, and the human hours to operate N sites. A 20 MB dependency-free binary you can push in a Helm values file changes the arithmetic of a two-hundred-site rollout versus a JVM cluster per site. Centrally, the calculus inverts — Kafka’s ecosystem of connectors, schema registry, and stream processors (see our Flink vs Spark Streaming vs Kafka Streams comparison) can save more engineering time than a leaner broker would. Which is the whole argument for bridging: run JetStream to the edge, then bridge into a central Kafka lakehouse so the analytics team keeps the tools they already know.
It helps to put rough numbers on the fleet arithmetic, because that is where the NATS JetStream vs Kafka decision is usually won or lost. Take two hundred sites. A JetStream leaf node holding a modest local buffer might reserve 256–512 MB of RAM per site — call it 100 GB of memory across the fleet, running on gateways you already deployed for protocol translation. A full Kafka broker per site, even a single-node one, realistically wants 8–12 GB once you account for heap and page cache; two hundred of those is 1.6–2.4 TB of memory you now have to provision, cool, and patch in the field. That is a fifteen-to-twenty-times difference in edge memory before you count the human cost of GC tuning and partition planning multiplied across the fleet. The number is illustrative, not a benchmark, but the order of magnitude is the point: the log broker’s strengths — throughput, retention, ecosystem — are strengths of a cluster, and the edge is the one place you cannot afford a cluster per node.
| Dimension | NATS JetStream | Apache Kafka (4.x, KRaft) |
|---|---|---|
| Footprint / binary | ~15–25 MB single Go binary, no deps; low-MB idle, hundreds of MB with streams | JVM cluster; 6 GB+ heap and 10–12 GB total memory per broker typical |
| Edge topology | Native leaf nodes + superclusters; store-and-forward built in | No native edge broker; MirrorMaker/replication or direct client connections |
| Delivery semantics | At-least-once; exactly-once publish in dedup window; effectively-once with idempotent consumers | At-least-once; transactional exactly-once for read-process-write in the cluster |
| Ordering | Per-subject order within a stream | Strict total order within a partition |
| Replay / retention | Stream limits by bytes/count/age; replay by sequence or time | Long retention; tiered storage to object store for months of history |
| Throughput ceiling | High; single node does millions/sec but cluster ceiling below Kafka’s | Very high; linear scaling with partitions to 7-figure msgs/sec |
| Ops burden | Low; one binary, small config, 3-node quorum for HA | Moderate; KRaft removed ZooKeeper but JVM/partition tuning remains |
| Ecosystem | Growing; KV, object store, services; fewer off-the-shelf connectors | Mature; Connect, schema registry, Flink/Streams, huge connector catalog |
One more capability worth naming: JetStream ships a KV store and an object store built on the same streams, so an edge node can hold device configuration, last-known-values, and small blobs without a second system. That consolidation — one binary doing messaging, KV, and object storage under one security context — is a quiet but real operational win at the edge that Kafka does not offer natively.
Trade-offs, gotchas, and what goes wrong
No broker is free of sharp edges, and the ones that cut you in production are rarely the ones in the marketing comparison. JetStream’s most common failure mode is storage exhaustion via consumer-ack pitfalls. If you use explicit ack (which you should for telemetry you cannot lose) but your consumer crashes, hangs, or simply forgets to ack, messages sit un-acked and get redelivered after AckWait — and if a downstream sink is wedged, the stream fills toward its MaxBytes limit. At that point your discard policy decides whether you reject new edge writes or silently evict old data. Neither is what you want during an incident, so you must size limits and alert on stream utilisation before it becomes a 3 a.m. page. The related trap is treating the deduplication window as a durability guarantee: it is a rolling time window (minutes), not permanent idempotency, so a duplicate publish that arrives after the window closes will be accepted. Design your consumers to be idempotent and you get effectively-once; lean on the dedup window alone and you will eventually double-count a metric.
The exactly-once caveat deserves restating because it trips up architects who read the feature list too fast. JetStream gives exactly-once publish into a stream within the window; it does not give exactly-once end-to-end processing, and it does not match Kafka’s transactional read-process-write across topics. If your compliance story requires transactional exactly-once over a multi-stage pipeline, that requirement points at Kafka, not JetStream — or at idempotent design regardless of broker.
Kafka’s failure modes at the edge are the mirror image: its weight and its assumption of a good link. Putting a broker at every site multiplies JVM memory, GC tuning, and patching across the fleet; connecting edge producers directly to central brokers means a WAN blip translates into producer buffer pressure and, eventually, dropped or blocking sends. Stretch clusters over high-latency links suffer because KRaft (like the old ZooKeeper quorum) wants low, stable inter-node latency for its consensus.

There is a subtler JetStream trap around replication and quorum that teams hit when they scale past a single edge node. A stream with Replicas: 3 uses a Raft group for durability, and Raft needs a majority to accept writes. If you naively place all three replicas across three sites connected by the same flaky WAN, a partition can cost you write availability on the stream precisely when the edge is busiest — the opposite of what you wanted. The correct pattern is to keep the durable, replicated streams inside a well-connected cluster (the regional hub or the core) and let leaf nodes at each site do local, single-replica store-and-forward that syncs upward. Getting this placement wrong is one of the most common reasons a first JetStream deployment feels “flaky” when the software is behaving exactly as designed.
Both systems share one genuinely nasty distributed-systems hazard worth calling out: reconnect storms and split-brain. When a regional link flaps and hundreds of NATS leaf nodes reconnect simultaneously, the hub sees a thundering herd of reconnections plus a burst of buffered store-and-forward traffic; without staggered reconnect backoff and hub capacity headroom, recovery itself can knock the hub over. The symmetric Kafka risk is a controller or partition-leader election storm during a network partition, where a mis-set min.insync.replicas can either stall writes or, if misconfigured, risk data loss on unclean leader election. The mitigation is the same discipline in both worlds: jittered reconnect backoff, capacity headroom for recovery bursts, and quorum settings that fail closed rather than open.
Practical recommendations
Start from the shape of your problem, not the popularity of the tool. If your defining constraint is topology — many small sites, DDIL links, ten-watt gateways, and a small ops team — NATS JetStream is the stronger default, and it is the protagonist of this ADR for good reason: it makes durable store-and-forward at the edge cheap and boring, which is exactly what you want telemetry infrastructure to be. If your defining constraint is a high-throughput central pipeline with heavy stream processing and a demand for the mature connector and schema ecosystem, Kafka remains the stronger default for that tier. And if you have both problems, which most real IIoT platforms do, bridge them: JetStream from the edge to a regional hub, a thin bridge service into a central Kafka cluster, and let each system do what it is best at. This is a systems recommendation based on mechanism and footprint, not an endorsement of either project’s brand.
Decision checklist:
- Choose NATS JetStream when: you have tens to thousands of edge sites; links are intermittent (DDIL); per-site compute is small; you need store-and-forward with local durability; a lean ops footprint matters more than a huge connector catalog; you also want edge KV/object storage from one binary.
- Choose Kafka when: your hard problem is central throughput into seven figures of messages per second; you need transactional exactly-once stream processing; you depend on the Connect/schema-registry/Flink ecosystem; you want months of retention via tiered storage; your topology is a few well-connected clusters, not a sprawling fleet.
- Choose both / bridge when: you have a wide edge and a heavy centre; run JetStream leaf nodes at sites, aggregate to a regional NATS cluster, and bridge into a central Kafka lakehouse — the common, pragmatic endpoint for 2026 IIoT platforms. If you are weighing central-tier options specifically, our Kafka vs Redpanda vs WarpStream edge telemetry ADR compares the log-broker contenders in depth.
Validate the bridge under failure before you trust it: kill the WAN at a test site, confirm the leaf buffers and resumes, and confirm no duplicates survive an idempotent consumer past the dedup window.
Frequently Asked Questions
Is NATS JetStream a drop-in replacement for Kafka?
No, and treating it as one leads to disappointment. They share the “durable, replayable log” concept, but the data models differ (subjects vs partitions), the ecosystems differ enormously (Kafka’s Connect/Streams/schema-registry world is far larger), and the exactly-once processing story differs. JetStream wins on footprint and edge topology; Kafka wins on central throughput and mature tooling. Pick by problem shape, and expect to port producers and consumers, not just swap a connection string.
Can NATS JetStream really run on a ten-watt industrial gateway?
Yes. A NATS server, including a JetStream leaf node, is a single dependency-free Go binary around 15–25 MB, idling at single-digit megabytes and rising to a few hundred with active streams. That fits a fanless ARM gateway comfortably. Kafka, even post-ZooKeeper on KRaft, is a JVM cluster wanting gigabytes of heap and page cache per broker, which is why running a full Kafka broker at every small site is usually impractical.
Does JetStream guarantee exactly-once delivery?
It guarantees exactly-once publish within a configurable deduplication window (minutes by default) using the Nats-Msg-Id header, plus at-least-once delivery to consumers. It does not provide exactly-once end-to-end processing — no distributed system truly does. The correct pattern is effectively-once: at-least-once delivery combined with idempotent consumers. If you need transactional read-process-write exactly-once across a pipeline, Kafka’s transactional model is stronger.
How does the NATS-to-Kafka bridge work in practice?
You run a small bridge service that consumes from JetStream streams (durable pull consumers) and produces into Kafka topics, or vice versa. It maps subjects to topics, carries the message ID for idempotency, and commits offsets/acks on both sides. The bridge lives centrally where the network is good, so the fragile edge hop is handled by JetStream’s store-and-forward and the Kafka side never sees the flaky WAN directly.
What is the biggest operational risk with JetStream at the edge?
Storage exhaustion from un-acked messages. If a downstream sink stalls and you use explicit acks, messages accumulate and the stream fills toward its byte/message/age limits, at which point your discard policy either rejects new edge writes or evicts old data. Size stream limits deliberately, alert on utilisation, and make consumers idempotent so redelivery is safe. The related risk is reconnect storms when many leaf nodes rejoin at once — use jittered backoff and hub headroom.
Is Kafka a bad choice for IIoT then?
Not at all — it is the wrong choice for the edge tier of a sprawling, poorly-connected fleet, but the right choice for the central tier where throughput, retention, and processing ecosystem dominate. The mistake is using one broker for both tiers. Kafka 4.x on KRaft with tiered storage is an excellent central telemetry backbone; pair it with a lightweight edge broker rather than stretching it to the field.
Further Reading
- NATS JetStream concepts — official documentation on streams, consumers, KV, and object store.
- NATS adaptive edge deployment — leaf nodes and superclusters for edge topologies.
- Apache Kafka documentation — KRaft, partitions, consumer groups, and tiered storage.
- MQTT Sparkplug B reference architecture — the gateway/protocol layer that feeds the broker.
- Kafka vs Redpanda vs WarpStream edge telemetry ADR — central-tier log-broker options.
- Flink vs Spark Streaming vs Kafka Streams — processing the telemetry once it lands.
By Riju — about
