Backend / Message queues / NATS / 01_nats_and_jetstream.md

NATS and JetStream

Updated 5 interview angles 4 min read source
On this page6
  1. Core NATS: the three patterns
  2. JetStream: streams and consumers
  3. Where it sits against the alternatives
  4. Why it comes up in AI work
  5. Related
  6. Interview angle

NATS and JetStream

Two systems in one binary, and the interview answer depends on knowing that.

Core NATS is fire-and-forget messaging: at-most-once, nothing stored, a subscriber that is offline simply misses the message. JetStream is the persistence layer on top: streams, replay, at-least-once.

Confusing the two is the tell. “NATS doesn’t persist” and “NATS is a Kafka alternative” are both half-right, and which half depends on JetStream.

Core NATS: the three patterns

Subjects are dot-delimited with wildcards — orders.eu.created, orders.*.created, orders.>.

Pattern Shape
Pub/sub every subscriber gets a copy
Request-reply built in, with a reply subject
Queue group one member of the group gets it

Request-reply is the underrated one. Most brokers make you build it out of two queues and a correlation id; in NATS it is a first-class operation, which makes NATS usable as a service-to-service RPC transport rather than only as a message bus.

Queue groups are how you get competing consumers: subscribers naming the same group share the messages, and adding a subscriber is instant — no partition count to rebalance.

In code

python
import nats

nc = await nats.connect("nats://localhost:4222")

# Request-reply, with a timeout
reply = await nc.request(
    "svc.echo", b"ping", timeout=0.5
)

# Queue group: work is split across members
await nc.subscribe(
    "jobs.>", queue="workers", cb=handle
)

Gotcha: core NATS drops messages for slow or absent consumers rather than buffering indefinitely. That is a deliberate design choice — it protects the cluster — and it means core NATS is the wrong answer for anything that must not be lost.

JetStream: streams and consumers

A stream captures subjects and stores messages with sequence numbers. A consumer is a server-side, stateful cursor over that stream — the server remembers your position, unlike Kafka where the client owns the offset.

python
js = nc.jetstream()

await js.add_stream(
    name="ORDERS", subjects=["orders.>"]
)

sub = await js.pull_subscribe(
    "orders.>", durable="billing"
)
for msg in await sub.fetch(10, timeout=2):
    await process(msg)
    await msg.ack()
Choice Options
Consumer delivery pull (batch fetch) or push
Consumer lifetime durable (survives) or ephemeral
Retention limits, interest, or work-queue

Retention is the design decision. limits keeps messages until an age, size or count bound — the Kafka-like behaviour that allows replay. workqueue deletes a message once acked, giving you a classic queue. So JetStream can be either shape depending on configuration, which is why “queue or log?” is a question you answer per stream rather than per technology.

At-least-once, and what that costs you

At-least-once. Messages survive restarts and can be replayed, and you will receive duplicates — so consumers must be idempotent, exactly as with Kafka or SQS. JetStream offers publish-side deduplication over a rolling window via a message id, which removes the common double-publish case but does not make end-to-end processing exactly-once.

Where it sits against the alternatives

NATS + JetStream Kafka
Ops weight one small binary heaviest
Latency lowest low, batched
Replay yes, per stream yes, core model
Ecosystem smallest largest

RabbitMQ sits between them: no replay, very low latency, moderate ops.

The honest positioning: NATS wins on operational simplicity and latency, Kafka wins on ecosystem and scale. A single static binary with clustering built in is a genuinely different operational story from a Kafka cluster, and for a small team that difference often outweighs the feature gap.

Where Kafka still wins outright: the connector ecosystem, stream processing (Flink, Kafka Streams), and the fact that your data platform probably already speaks it.

The extras worth naming

JetStream also ships a key-value store and an object store built on streams. The KV store is genuinely useful for configuration and coordination — it gives you watch semantics for free — and it means a NATS deployment can sometimes replace a separate Redis for light coordination work.

Why it comes up in AI work

Agent systems care about the two things NATS is good at: low-latency request-reply between services, and durable subject-based fan-out when a run completes. A document-processing pipeline where each stage is a consumer maps onto subjects cleanly, and the KV store covers run state without another dependency.

See Event-driven agents.

Interview angle 5

  • “What is NATS?” - two things in one binary. Core NATS is at-most-once fire-and-forget messaging with pub/sub, request-reply and queue groups; JetStream is the persistence layer on top giving streams, replay and at-least-once. Saying “NATS doesn’t persist” without that distinction is the giveaway.
  • “How does it compare to Kafka?” - NATS wins on operational simplicity and latency, Kafka on ecosystem and scale. A single clustered binary versus a Kafka cluster is a real difference for a small team. Kafka keeps the connectors, the stream-processing tooling, and the fact that your data platform already speaks it.
  • “Queue or log?” - a per-stream decision in JetStream, set by retention policy. workqueue deletes on ack and behaves like a queue; limits keeps messages by age or size and allows replay like a log.
  • “Who tracks the consumer position?” - the server, unlike Kafka where the client commits offsets. That makes consumers simpler and means adding one is instant, with no partition count to rebalance against.
  • “What’s the catch with core NATS?” - it drops messages for slow or absent consumers rather than buffering. That is deliberate and protects the cluster, but it means core NATS is the wrong choice for anything that must not be lost — that is what JetStream is for.