NATS
NATS is a fast messaging system with publish-subscribe, request-reply and JetStream persistence. Learn how it works and how it supports tabular data pipelines.
Overview
NATS is a lightweight, fast messaging system for connecting services. Programs publish messages to named subjects, and other programs subscribe to those subjects. The core system is simple: a message goes to whoever is listening at that moment, with very low latency. It supports publish-subscribe, request-reply and queue groups for sharing work. A layer called JetStream adds persistence, replay and stronger delivery guarantees when they are needed.
NATS is popular for low-latency service communication in cloud native systems. A small server binary, a text-like protocol and a clear subject naming model make it easy to run and to reason about. For teams building data pipelines around tabular foundation models, NATS can carry row batches, job requests and prediction results between services. This article explains how it works, how it differs from RabbitMQ and Apache Kafka, and where it fits in pipelines that feed models learning from raw tables.
What it is
NATS is an open source messaging system written in Go. A NATS server routes messages between clients. Several servers can be joined into a cluster, and clusters can be joined into larger topologies called superclusters through gateways, or extended to the edge through leaf nodes.
Core NATS and JetStream
| Layer | Delivery style | Persistence | Typical use |
|---|---|---|---|
| Core NATS | At most once, fire and forget | None | Fast notifications, request-reply, service calls |
| JetStream | At least once, optional exactly once with deduplication | Streams stored on disk or memory | Durable work, replay, event history |
| Key-value and object stores | Built on JetStream streams | Persistent | Configuration, small state, larger blobs |
An everyday analogy
Core NATS is like a public address system in a train station. The announcer speaks and everyone who is in earshot hears it right now. If you were out of the room, you missed it. JetStream is like adding a recording system with numbered tapes. Each announcement is stored, and a listener who arrives late can ask for the tapes from a given point and play them back.
Subjects
A subject is a dot-separated string such as sensors.eu.temperature. Subscribers can use wildcards. The token wildcard * matches one token, and the full wildcard > matches one or more trailing tokens. So sensors.*.temperature matches the Europe subject and others, and sensors.> matches everything below sensors.
Messages
A NATS message has a subject, an optional reply subject, optional headers and a payload of bytes. The server does not interpret the payload. It may hold JSON, Protobuf, Arrow data or a pointer to a file.
Where it came from
NATS was created in the early 2010s by an engineer who wanted a simple, fast messaging layer for a cloud platform. The first server was written in Ruby and was later rewritten in Go for performance and easy deployment. The project is now a graduated or incubating member of a major cloud native foundation, which signals broad community use.
The early system offered only the core, at most once model. This was intentional. The design goal was a fast nervous system for distributed services, not a storage system. As users asked for durability, an earlier streaming add-on appeared and was later replaced by JetStream, which was built directly into the server. JetStream added streams, consumers, key-value buckets and object stores in the same binary.
Design philosophy in general terms
- Keep the server small and the protocol simple.
- Make the common case, publish and subscribe, as cheap as possible.
- Push complexity to optional layers rather than the core.
- Be easy to run on a laptop, in a container or at the edge.
- Support many languages through small client libraries.
Exact release dates and version features are best confirmed in the official documentation of the version you use.
How it works
Step 1: connect
A client opens a connection to any server in the cluster. The server tells the client about other servers, so the client can fail over automatically. Connections are cheap, and a single connection can carry many subscriptions.
Step 2: subscribe
A client declares interest in a subject or pattern. The server keeps an interest graph so that it only forwards messages where someone is listening. This is like a mailing list that only sends mail to people who signed up.
Step 3: publish
A publisher sends a message to a subject. The server looks up matching subscriptions and delivers a copy to each one. If nobody is subscribed, the message is dropped in core NATS. That is the at most once model.
Step 4: request and reply
For request-reply, the requester publishes to a subject and includes a unique reply subject, called an inbox. A responder receives the request, does the work and publishes the answer to the reply subject. Many clients wrap this in a single call with a timeout. If several responders are subscribed, the first answer wins, and the system can add load balancing through queue groups.
Step 5: queue groups
Subscribers can join a queue group. When a message arrives on the subject, the server delivers it to only one member of the group, chosen at random. This gives simple load balancing without any central queue. Scaling a service means starting another instance with the same queue group name.
Step 6: add JetStream for durability
A stream captures messages published to a set of subjects and stores them. Stream settings include retention policy, storage type, replica count and limits.
| Setting | Options | Meaning |
|---|---|---|
| Retention | Limits, interest, work queue | When messages may be removed |
| Storage | File, memory | Where messages are kept |
| Replicas | 1, 3, 5 | Copies across servers using a consensus protocol |
| Limits | Max age, max bytes, max messages | Bounds on stream size |
| Discard | Old, new | What to drop when limits are reached |
A consumer is a view onto a stream with its own position. Consumers can be durable, which means the server remembers where they left off, or ephemeral. They can be push based or pull based. Pull consumers are common for batch workers, because the worker asks for the next batch when it is ready.
Step 7: acknowledge
With JetStream consumers, each message must be acknowledged. The consumer can acknowledge, negatively acknowledge to request redelivery, or signal that it is still working to extend the deadline. Unacknowledged messages are redelivered after a timeout up to a maximum count.
Step 8: deduplication
A publisher can set a message identifier header. The stream remembers identifiers for a window of time and ignores duplicates. Combined with acknowledgements, this supports effectively once processing.
Key-value and object stores
JetStream also provides a key-value store, where each key maps to a value with history and watch support, and an object store for larger payloads chunked into messages. These are convenient for configuration, small shared state and moving artifacts such as model files or reference tables, though they are not substitutes for a full database or a lake.
Key concepts and terms
| Term | Meaning |
|---|---|
| Subject | A dot-separated name that messages are published to |
| Wildcard | * for one token, > for the rest of the subject |
| Inbox | A unique reply subject used for request-reply |
| Queue group | Subscribers that share the messages on a subject |
| JetStream | The persistence layer built into the server |
| Stream | A stored sequence of messages for a set of subjects |
| Consumer | A stateful view of a stream with its own cursor |
| Pull consumer | A consumer where the client asks for batches |
| Ack | A confirmation that a message was processed |
| Cluster | A group of servers acting as one system |
| Leaf node | A server that extends a cluster to an edge or other domain |
| Gateway | A link between clusters in a supercluster |
| Account | An isolated namespace with its own subjects and limits |
| Key-value bucket | A key-value store built on a stream |
| Object store | A store for larger blobs built on a stream |
| Subject mapping | A rule that rewrites subjects as they enter an account |
A worked example
A service must score uploaded tables. Clients send a job request and expect a result. Workers should scale out, and failed jobs should be retried.
Core request-reply with a queue group
The example uses the Python client nats-py. The server address is a placeholder.
import asyncio
import json
import nats
async def worker():
nc = await nats.connect("nats://nats.example.internal:4222")
async def handle(msg):
job = json.loads(msg.data)
result = {"job_id": job["job_id"], "status": "ok", "rows": job["rows"]}
await msg.respond(json.dumps(result).encode())
await nc.subscribe("score.table", queue="scorers", cb=handle)
await asyncio.Event().wait()
asyncio.run(worker())
async def client():
nc = await nats.connect("nats://nats.example.internal:4222")
req = {"job_id": "j-001", "rows": 5000}
reply = await nc.request("score.table", json.dumps(req).encode(), timeout=5)
print(json.loads(reply.data))
With three workers in the scorers group, each request goes to exactly one of them.
Adding JetStream for durable jobs
Request-reply fails if no worker is up. For work that must not be lost, create a stream and a pull consumer.
async def setup_and_publish():
nc = await nats.connect("nats://nats.example.internal:4222")
js = nc.jetstream()
await js.add_stream(
name="JOBS",
subjects=["jobs.>"],
retention="workqueue",
storage="file",
num_replicas=3,
max_age=24 * 3600,
)
ack = await js.publish(
"jobs.score",
b'{"job_id": "j-002", "path": "s3://bucket.example.internal/t.parquet"}',
headers={"Nats-Msg-Id": "j-002"},
)
print(ack.seq)
async def pull_worker():
nc = await nats.connect("nats://nats.example.internal:4222")
js = nc.jetstream()
sub = await js.pull_subscribe("jobs.score", durable="scorer")
while True:
try:
msgs = await sub.fetch(batch=10, timeout=5)
except nats.errors.TimeoutError:
continue
for m in msgs:
try:
process(m.data)
await m.ack()
except Exception:
await m.nak(delay=10)
What happens
- The publisher writes to
jobs.score. The stream stores it on three replicas and returns an acknowledgement with a sequence number. - The duplicate guard uses
Nats-Msg-Id, so a retried publish does not create a second job. - A pull worker fetches up to ten messages, processes them and acknowledges each.
- A failed job is negatively acknowledged with a delay, then redelivered.
- With the work queue retention policy, acknowledged messages are removed from the stream.
Numeric walk-through
Suppose 1,000 jobs arrive, and one worker handles about 5 jobs per second. One worker needs 200 seconds. With a queue group or a shared durable pull consumer and 8 workers, the same load needs about 25 seconds if the work is evenly sized. The subject-based design means adding workers needs no change to publishers. Setting a maximum number of unacknowledged messages per consumer, called max ack pending, caps how much work is in flight and protects downstream systems.
Strengths and limitations
Strengths
- Very low latency and high message rates for small messages.
- Small footprint. A single binary, easy to run in containers and at the edge.
- Simple subject model with wildcards that map naturally to hierarchies.
- Built-in request-reply and queue groups.
- Self-healing clusters with automatic client reconnection.
- JetStream adds persistence without a separate system.
- Multi-tenancy through accounts, with controlled import and export of subjects.
- Flexible topologies, from one node to global superclusters and edge leaf nodes.
- Many client libraries.
Limitations
- Core NATS drops messages when nobody is listening. It is not a durable queue by itself.
- Subjects are strings. There is no schema enforcement, so teams must manage message formats themselves.
- JetStream is younger than long-standing log and broker systems, and its operational patterns are still developing in many shops.
- Complex routing by message content is not built in. Subjects carry most of the routing logic.
- Very large payloads are not a good fit. Use pointers to object storage.
- Ordering across multiple subscribers or partitions needs careful design.
- Stream-processing features such as windowing and joins are outside its scope. Pair it with an engine such as Apache Flink.
Fit summary
| Need | Fit |
|---|---|
| Fast service-to-service calls | Strong |
| Fan-out notifications | Strong |
| Durable job queue | Good with JetStream |
| Long retained event history for analytics | Possible, but log systems are more common |
| Complex content-based routing | Weaker than a broker with rich exchange types |
| Constrained devices | Good, though MQTT is the usual device protocol |
How it compares with alternatives
| Feature | NATS | RabbitMQ | Apache Kafka | MQTT broker |
|---|---|---|---|---|
| Primary model | Subjects, with optional streams | Exchanges and queues | Partitioned log | Topics for devices |
| Default durability | None in core, optional with JetStream | Optional per queue | Always persisted | Optional |
| Routing | Subject wildcards | Rich exchange types | Topic and key | Topic wildcards |
| Request-reply | Built in | Pattern with reply-to | Not native | Limited |
| Footprint | Very small | Moderate | Larger | Small |
| Replay | JetStream | Streams | Native | Retained message only |
| Strong suit | Latency, simplicity, edge | Task queues, routing | High-throughput event log | Many small devices |
See the sibling pages for RabbitMQ, Apache Kafka and MQTT. Messaging choices also relate to event-driven architecture and remote calls with gRPC. NATS can replace direct calls in some designs, but it does not replace a service contract. Teams still need agreed message formats.
NATS and gRPC
gRPC gives typed contracts and streaming over HTTP/2 between known endpoints. NATS gives location-independent addressing: callers publish to a subject and do not need to know which instance answers. Some teams use both, with gRPC for typed internal APIs and NATS for events and discovery-free routing.
Role in tabular foundation models
NATS does not understand tables and does not train or run models. Its role is to carry the data and the control messages that surround models that learn from raw tables. The points below stay general and avoid claiming specific product integrations.
Where table models need messaging
A tabular foundation model reads a table as rows and columns, handles mixed numeric, categorical, text and date types, tolerates missing values and may see columns with no helpful names. When used with in-context learning, it receives labeled example rows plus query rows in one pass and predicts without a gradient update. That serving pattern creates many small, latency-sensitive requests as well as heavy batch jobs. See the model architecture page for how such a model reads a table.
Concrete uses
- Low-latency inference calls. A feature service sends a few rows to a model service by request-reply and gets predictions back within a deadline. Queue groups spread requests across model replicas.
- Batch job queues. JetStream work queue streams hold imputation, scoring and anomaly jobs. Pull consumers let each GPU worker take only as much as it can handle.
- Event streams of rows. Operational systems publish row-level events on subjects such as
orders.created. A scoring service subscribes, calls the model and publishes flags onorders.anomaly. This maps well to anomaly detection with scoring close to the data source. - Edge to core. Leaf nodes at factories or branches collect rows locally and forward them to a central cluster when links allow. Local scoring can continue during outages.
- Control plane. Key-value buckets can hold model version pointers, schema versions and feature flags. Services watch a key and switch to a new model version when it changes.
- Continual learning feed. Newly labeled rows are appended to a stream. A scheduled job reads from a saved position, updates or evaluates a model and records the new version in the key-value store.
- Artifact distribution. The object store can move small model files or reference tables to workers, though large artifacts are usually better served from dedicated storage.
- Semantic and sector tagging. A column or table classification service subscribes to
tables.uploaded, tags columns with semantic types or sectors and publishes results to a catalog service. See data catalog.
Mapping tasks to NATS features
| Task | NATS feature |
|---|---|
| Real-time prediction | Request-reply with queue groups |
| Bulk imputation | JetStream work queue with pull consumers. Modeling ideas are in imputation networks |
| Streaming anomaly flags | Subject hierarchy per source; subscriber publishes flags |
| Model rollout | Key-value watch on a version key |
| Replay for evaluation | Consumers that start from a chosen sequence or time |
| Synthetic data jobs | Work queue stream for generation tasks. See synthetic data generation |
Design notes
- Encode the schema version and model version in headers so that consumers can route or reject.
- Send row batches in a compact columnar or binary format, or send a pointer to stored data for large tables.
- Use message identifiers for deduplication and idempotent writes.
- Protect sensitive columns before publishing. Options include data tokenization and format-preserving encryption.
- Use accounts to separate tenants so that one team's subjects are invisible to another.
- Measure end-to-end latency, including queueing, packing the context and model time. Messaging is usually the smallest part, but backlog is the first thing to grow when a model server slows.
For background on training and evaluation of such models, see research and benchmarks.
Practical guidance and best practices
Subject design
- Use a stable hierarchy such as
domain.entity.event, for exampletables.uploadedorrows.scored.v1. - Put versions in the subject or headers.
- Avoid putting high-cardinality values such as user identifiers in the early tokens, since wildcard design gets harder.
- Document subjects as an API.
Choosing core or JetStream
- Use core for fast, ephemeral calls where a lost message is acceptable or the caller retries.
- Use JetStream when a lost message would be a problem, when you need replay, or when workers may be offline.
- Do not turn on JetStream for everything by default. It adds storage and operational cost.
JetStream tuning
- Choose three replicas for important streams.
- Set max age, max bytes and discard policy so streams cannot grow without bound.
- Use pull consumers for batch workers and set a sensible ack wait and max deliver.
- Use a deduplication window that covers your longest publisher retry period.
- Use work queue retention for jobs and limits retention for event history.
Security
- Use TLS for client and server connections.
- Use accounts and per-user permissions that limit publish and subscribe subjects.
- Prefer token-based or key-based credentials issued per application, stored in a secrets manager such as HashiCorp Vault.
- Follow zero trust ideas: authenticate every client and grant minimum subject access.
Deployment
# Illustrative server configuration for a three-node cluster node
server_name: nats-1
listen: 0.0.0.0:4222
http: 8222
jetstream:
store_dir: /data/jetstream
max_memory_store: 1GB
max_file_store: 50GB
cluster:
name: example-cluster
listen: 0.0.0.0:6222
routes:
- nats://nats-2.example.internal:6222
- nats://nats-3.example.internal:6222
On Kubernetes a chart or operator is common. For local work use a container with Docker.
Observability
- Scrape server monitoring endpoints and export metrics.
- Watch slow consumers, pending message counts, redelivery counts and stream sizes.
- Propagate trace context in headers with OpenTelemetry so that a request can be followed across services.
- Alert when a consumer falls far behind its stream.
Client behavior
- Always set timeouts on requests.
- Handle disconnect and reconnect events.
- Drain connections on shutdown so in-flight messages complete.
- Cap max ack pending so a stuck consumer does not hold too many messages.
- Keep handlers idempotent.
Delivery guarantees explained
| Goal | How to approach it |
|---|---|
| Do not lose accepted work | JetStream stream with three replicas and publish acknowledgements |
| Do not process twice | Message identifiers plus idempotent writes in the consumer |
| Keep order for one entity | Publish all messages for that entity to one subject and use one consumer for it |
| Handle poison messages | Cap deliveries, then route the failure to an advisory or parking subject |
| Apply backpressure | Pull consumers and a low max ack pending |
Exactly once processing is a property of the whole system, not of the broker. The broker can remove duplicates at the entrance, but only an idempotent consumer can remove them at the exit.
Multi-tenancy and isolation
Accounts are the main isolation tool. Each account has its own subject space. Two accounts cannot see each other's messages unless one exports a subject and the other imports it. This is like separate apartments in one building with a shared mail slot that both sides agree to use.
- Give each team or customer its own account.
- Export only the subjects that should be shared.
- Set limits on connections, subscriptions and JetStream storage per account.
- Review imports and exports as part of change control.
Edge patterns
Leaf nodes let a small server at a site connect to a central cluster. The local server keeps working if the link drops, and messages flow again when it returns. For tabular workloads this means local systems can keep publishing rows and receiving local scores during an outage. Streams can be mirrored or sourced from the edge into a central stream so that history is eventually consolidated.
Migration tips from other systems
- Start by mapping queues or topics to subjects and decide where durability is needed.
- Run old and new systems side by side with a bridge while consumers move over.
- Keep message formats unchanged during the first phase to separate risks.
- Move the least critical flows first, then the high-volume ones.
- Rehearse failure cases before cutting over.
Sizing a first deployment
- Three servers with fast local disks for JetStream.
- Separate disks for the JetStream store if possible.
- Memory sized for connections and in-memory streams, with headroom.
- A load test that mirrors real payload sizes and consumer speeds.
- A written runbook for node replacement and stream restore.
A pipeline sketch for table scoring
| Stage | Subject or store | Purpose |
|---|---|---|
| Upload notice | tables.uploaded |
A service announces a new table with a pointer |
| Validation | jobs.validate |
Workers check schema and types |
| Scoring | jobs.score |
GPU workers pull batches and call the model |
| Results | results.scored |
Downstream writers store predictions |
| Failures | jobs.failed |
Parking subject for inspection |
| Control | key-value bucket config |
Model and schema version pointers |
Each stage can be scaled on its own, and each can be replayed from its stream when a bug is fixed.
Common mistakes
- Expecting durability from core NATS. If no subscriber is present, the message is gone. Add JetStream when durability matters.
- Using requests without timeouts. A missing responder can hang a caller. Always set a deadline.
- Overusing wildcards. Broad subscriptions such as
>in production can drain performance and expose data. - No limits on streams. A stream with no age or size limit eventually fills the disk.
- Forgetting to acknowledge. Unacknowledged messages are redelivered and can cause duplicate work or loops.
- Single replica for critical streams. One node failure then risks data loss.
- Sending huge payloads. Large bodies add latency and memory pressure. Send pointers.
- Skipping deduplication ids. Publisher retries then create duplicates.
- No subject conventions. Ad hoc subject names become impossible to manage across teams.
- Sharing one account for everyone. Accounts exist to isolate teams and limit blast radius.
- Treating the key-value store as a database. It is for small state and configuration, not for analytics queries.
- Ignoring slow consumers. A slow subscriber can be disconnected by the server to protect others. Monitor and scale consumers.
Frequently asked questions
What is NATS used for?
NATS is used to connect services with fast publish-subscribe, request-reply and work distribution. Teams use it for microservice communication, event notifications, edge to cloud links and lightweight job queues. With JetStream, it can also store messages for replay and guaranteed delivery.
What is JetStream in NATS?
JetStream is the persistence layer built into the NATS server. It lets you define streams that store messages on disk or in memory, replicate them across servers and serve them to consumers that track their own position. It adds acknowledgements, redelivery, replay, deduplication and the key-value and object stores.
Is NATS a replacement for Kafka?
Not exactly. Kafka is a partitioned log designed for very high throughput event streaming with long retention and a large ecosystem of stream processing tools. NATS with JetStream can cover many streaming needs with a smaller footprint and simpler operations, but the two have different strengths. The right choice depends on volume, retention needs and the surrounding tools.
How does NATS compare with RabbitMQ?
RabbitMQ offers rich server-side routing through exchange types and per-queue features such as priorities and dead lettering. NATS offers a simpler subject model, very low latency, built-in request-reply and a small footprint. RabbitMQ is often chosen for task queues with complex routing. NATS is often chosen for fast service communication and edge deployments.
Does NATS guarantee message delivery?
Core NATS offers at most once delivery, so a message can be lost if no subscriber is connected or if a subscriber is too slow. JetStream provides at least once delivery with acknowledgements and redelivery, and it can approach exactly once behavior when combined with deduplication and idempotent handlers.
What is a queue group?
A queue group is a set of subscribers that share a subject. Each message is delivered to only one member of the group, so adding more members spreads the load. This gives simple horizontal scaling without a central queue object.
Can NATS carry large tables?
It can carry moderately sized payloads, but large tables are better stored in object storage or a database with only a pointer and a checksum sent through NATS. Large messages increase latency and memory use and can hurt other traffic on the same server.
How does NATS help machine learning systems?
It moves requests, rows, job messages and results between the services around a model. It can provide low-latency inference calls, durable job queues, event streams for scoring and a small key-value store for model version pointers. It does not itself train or serve models.
Is NATS secure for sensitive data?
NATS supports TLS, authentication and fine-grained permissions on subjects, and accounts isolate tenants. Security still depends on configuration and on data handling. Sensitive fields should be protected with methods such as tokenization or encryption before they are published, and credentials should be kept in a secrets manager.
How many servers should a production NATS cluster have?
Three servers is the common starting point because JetStream replication uses a majority-based protocol. Three replicas tolerate the loss of one server. Larger clusters add capacity and fault tolerance but also coordination overhead, so size them from measured load.
Glossary
- Account: An isolated namespace for subjects, users and limits.
- Ack wait: The time a consumer has to acknowledge before redelivery.
- Cluster: Servers that act together as one logical system.
- Consumer: A stateful reader of a stream.
- Deduplication window: The time during which repeated message identifiers are ignored.
- Drain: A graceful shutdown that lets in-flight messages finish.
- Gateway: A connection between clusters in a supercluster.
- Inbox: A unique temporary subject used for replies.
- JetStream: The built-in persistence and streaming layer.
- Key-value bucket: A keyed store built on a stream with history and watches.
- Leaf node: A server that links an edge or separate domain to a core cluster.
- Max ack pending: A cap on unacknowledged messages for a consumer.
- Pull consumer: A consumer where the client requests batches.
- Queue group: Subscribers that share messages on a subject.
- Request-reply: A pattern where a caller sends a request and awaits a response.
- Stream: A persisted ordered sequence of messages.
- Subject: The name used to route messages.
- Wildcard: A token that matches one or many subject parts.
Summary
NATS is a small, fast messaging system built around subjects. Core NATS gives publish-subscribe, request-reply and queue groups with minimal overhead, and JetStream adds durable streams, consumers, replay, deduplication and key-value and object stores. It suits low-latency service communication, edge to cloud links and lightweight job queues, and it is less suited to complex content-based routing or very large payloads. In tabular foundation model pipelines, it carries row batches, scoring requests, anomaly flags and control messages, and it can hold model version pointers, while the models themselves handle mixed-type columns, missing values and in-context prediction. Choose it when simplicity and latency matter, compare it with RabbitMQ and Apache Kafka, and pair it with strong security and idempotent consumers.
Related
It belongs to Event streaming in my stack, under Data engineering and lakehouse. Others in the same group: