NATS JetStream — Ultra-Lightweight Messaging for Event-Driven Microservices
Posted on: 4/27/2026 9:11:04 AM
Table of contents
- What is NATS and Why Does It Matter?
- NATS Core: Ultra-Lightweight Communication Foundation
- JetStream: Persistence and Streaming
- Key-Value Store: Built-in State Management
- Cluster Architecture and Edge Deployment
- NATS vs Kafka vs RabbitMQ Comparison
- NATS Integration with .NET and Aspire
- Real-World Pattern: Saga with NATS JetStream
- Monitoring and Observability
- When Should You Choose NATS?
- Conclusion
- References
In the microservices world, reliable and efficient inter-service communication is a fundamental challenge. NATS — an open-source messaging system written in Go — has quietly become a top choice for event-driven architectures thanks to its extreme performance, ultra-light footprint, and flexible deployment model. With JetStream — its built-in persistence layer — NATS is no longer just a simple pub/sub system but a full-featured streaming platform that competes directly with Kafka and RabbitMQ.
What is NATS and Why Does It Matter?
NATS (Neural Autonomic Transport System) was created by Derek Collier — who previously built messaging systems at TIBCO. NATS's design philosophy is fundamentally different from Kafka or RabbitMQ:
- Simplicity first: A single binary, minimal configuration, text-based protocol that's easy to debug.
- Always on, always available: Self-healing clusters, automatic reconnection, no human intervention needed.
- Multi-pattern: Pub/Sub, Request/Reply, Queue Groups, Key-Value Store, Object Store — all in one.
- Location transparency: Clients don't need to know where services are running — NATS handles routing automatically.
graph TB
subgraph "NATS Unified Platform"
A[NATS Core] --> B[Pub/Sub]
A --> C[Request/Reply]
A --> D[Queue Groups]
E[JetStream] --> F[Streaming]
E --> G[Key-Value Store]
E --> H[Object Store]
E --> I[Exactly-Once Delivery]
end
J[Microservice A] --> A
K[Microservice B] --> A
L[Microservice C] --> E
M[Edge Device] --> A
style A fill:#e94560,stroke:#fff,color:#fff
style E fill:#2c3e50,stroke:#fff,color:#fff
style B fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style D fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style F fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style G fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style H fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style I fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style J fill:#4CAF50,stroke:#fff,color:#fff
style K fill:#4CAF50,stroke:#fff,color:#fff
style L fill:#4CAF50,stroke:#fff,color:#fff
style M fill:#4CAF50,stroke:#fff,color:#fff
NATS — a unified messaging platform for all communication patterns
NATS Core: Ultra-Lightweight Communication Foundation
NATS Core provides at-most-once messaging with exceptional performance. It's the ideal communication layer for use cases that don't require persistence — such as health checks, service discovery, or real-time notifications.
Pub/Sub — Fire and Forget
The simplest pattern: a publisher sends a message to a subject, and all active subscribers receive it.
// Publisher
await nats.PublishAsync("orders.created", orderData);
// Subscriber
await foreach (var msg in nats.SubscribeAsync<Order>("orders.created"))
{
await ProcessOrder(msg.Data);
}
Request/Reply — Synchronous over Async
NATS turns async messaging into a request/reply pattern, allowing microservices to communicate synchronously without direct knowledge of each other.
// Service A — request
var reply = await nats.RequestAsync<OrderRequest, OrderResponse>(
"orders.validate", new OrderRequest { Id = 42 });
// Service B — reply handler
await foreach (var msg in nats.SubscribeAsync<OrderRequest>("orders.validate"))
{
var result = await ValidateOrder(msg.Data);
await msg.ReplyAsync(result);
}
Queue Groups — Automatic Load Balancing
When multiple instances of the same service subscribe to the same subject with a queue group, NATS automatically distributes messages in round-robin fashion — no separate load balancer needed.
// 3 instances subscribing — NATS auto load-balances
await foreach (var msg in nats.SubscribeAsync<Order>(
"orders.process", queueGroup: "order-processors"))
{
await ProcessOrder(msg.Data);
}
Queue Groups vs Kafka Consumer Groups
Unlike Kafka, which requires partitions for parallelism, NATS Queue Groups work on any subject without pre-configuration. Adding or removing consumer instances is fully automatic — zero configuration required.
JetStream: Persistence and Streaming
JetStream is a persistence layer integrated directly into NATS Server (enabled with a single config line). It transforms NATS from a pure messaging system into a full-featured streaming platform.
graph LR
A[Producer] -->|Publish| B[Stream]
B -->|"Retention: Limits/Interest/WorkQueue"| C[Storage Layer]
C -->|File/Memory| D[(Raft Consensus)]
B --> E[Consumer 1
Durable, Pull]
B --> F[Consumer 2
Push, Ephemeral]
B --> G[Consumer 3
Ordered]
style A fill:#4CAF50,stroke:#fff,color:#fff
style B fill:#e94560,stroke:#fff,color:#fff
style C fill:#2c3e50,stroke:#fff,color:#fff
style D fill:#2c3e50,stroke:#fff,color:#fff
style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style F fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style G fill:#f8f9fa,stroke:#e94560,color:#2c3e50
JetStream architecture — Streams, Storage, and Consumers
Streams — Where Messages Are Stored
A Stream is the storage unit in JetStream and can capture messages from multiple subjects. Three retention modes:
| Retention Policy | Description | Use Case |
|---|---|---|
| Limits | Keeps messages by size/count/age — oldest deleted first | Event logs, audit trails |
| Interest | Deletes messages when all consumers have acknowledged | Task queues with multiple worker groups |
| WorkQueue | Deletes messages as soon as any consumer acknowledges | Job processing, exclusive consumers |
Exactly-Once Delivery
JetStream supports exactly-once through a combination of message deduplication (producer side) and double ack (consumer side):
// Producer: deduplication via Nats-Msg-Id header
var headers = new NatsHeaders { { "Nats-Msg-Id", $"order-{orderId}" } };
var ack = await js.PublishAsync("orders.created", orderData,
opts: new NatsJSPubOpts { MsgId = $"order-{orderId}" });
// Consumer: double ack ensures exactly-once processing
await foreach (var msg in consumer.ConsumeAsync<Order>())
{
await ProcessOrderIdempotently(msg.Data);
await msg.AckAsync(); // Server confirms processing
}
Exactly-Once Isn't Free
The deduplication window defaults to 2 minutes. Producers must assign a unique Nats-Msg-Id to each message. Consumers still need idempotent processing — JetStream guarantees delivery, not business logic correctness.
Key-Value Store: Built-in State Management
JetStream provides an immediately-consistent Key-Value Store — no need for a separate Redis or etcd for configuration, feature flags, or service registry.
// Create KV bucket
var kv = await js.CreateKeyValueStoreAsync(new NatsKVConfig("config")
{
History = 5, // Keep last 5 versions
MaxBytes = 1024 * 1024
});
// Put/Get
await kv.PutAsync("feature.dark-mode", "enabled");
var entry = await kv.GetEntryAsync<string>("feature.dark-mode");
// Watch changes — real-time notifications
await foreach (var update in kv.WatchAsync<string>("feature.>"))
{
Console.WriteLine($"Config changed: {update.Key} = {update.Value}");
}
Notably, the KV Store supports history — you can view previous values of a key, a feature that Redis doesn't natively offer.
Cluster Architecture and Edge Deployment
NATS has the most flexible topology model among current messaging systems:
graph TB
subgraph "US Region — Supercluster"
US1[NATS Node 1] --- US2[NATS Node 2]
US2 --- US3[NATS Node 3]
US3 --- US1
end
subgraph "EU Region — Supercluster"
EU1[NATS Node 1] --- EU2[NATS Node 2]
EU2 --- EU3[NATS Node 3]
EU3 --- EU1
end
subgraph "Edge — Leaf Nodes"
L1[Factory Floor
Leaf Node]
L2[IoT Gateway
Leaf Node]
L3[Dev Machine
Leaf Node]
end
US1 ---|Gateway| EU1
US2 --> L1
EU2 --> L2
US3 --> L3
style US1 fill:#e94560,stroke:#fff,color:#fff
style US2 fill:#e94560,stroke:#fff,color:#fff
style US3 fill:#e94560,stroke:#fff,color:#fff
style EU1 fill:#2c3e50,stroke:#fff,color:#fff
style EU2 fill:#2c3e50,stroke:#fff,color:#fff
style EU3 fill:#2c3e50,stroke:#fff,color:#fff
style L1 fill:#4CAF50,stroke:#fff,color:#fff
style L2 fill:#4CAF50,stroke:#fff,color:#fff
style L3 fill:#4CAF50,stroke:#fff,color:#fff
NATS Superclusters + Leaf Nodes — messaging from cloud to edge
Superclusters
Multiple NATS clusters connected via Gateways form a supercluster. Messages automatically route between clusters/regions without clients needing to know the topology. Adding a new region? Just connect a gateway — zero downtime, zero client changes.
Leaf Nodes — Edge Computing with NATS
Leaf Nodes are lightweight NATS server instances that connect to the main cluster/supercluster. Key features:
- Disconnected operation: When internet connectivity is lost, the leaf node continues operating independently with local JetStream.
- Automatic sync: When connectivity is restored, messages automatically synchronize to the hub.
- Security boundary: Leaf nodes can restrict allowed subjects, creating natural multi-tenancy.
Real-world Use Case
A manufacturing plant deploys a leaf node at each production line, collecting sensor data in real-time. When the WAN connection drops, the line keeps running normally — data buffers locally and syncs to the cloud when connectivity is restored. This pattern is ideal for IoT and edge computing scenarios.
NATS vs Kafka vs RabbitMQ Comparison
| Criteria | NATS JetStream | Apache Kafka | RabbitMQ |
|---|---|---|---|
| Throughput (persistent) | 200K-400K msg/s | 500K-1M+ msg/s | 50K-100K msg/s |
| Latency | Sub-ms (core), 1-5ms (JetStream) | 10-50ms (batching) | 5-20ms |
| Operational complexity | Very low — single binary | High — KRaft/ZK, schema registry | Medium — Erlang runtime |
| Multi-pattern | Pub/Sub, Req/Reply, Queue, KV, Object Store | Pub/Sub, Streams | Pub/Sub, Queue, RPC |
| Edge deployment | Native Leaf Nodes | No native support | Federation/Shovel |
| Memory footprint | ~20MB | ~1GB+ | ~150MB |
| Best for | Microservices, IoT, Edge, Request/Reply | Event streaming, Data pipelines, Log aggregation | Enterprise messaging, Task queues |
NATS Integration with .NET and Aspire
The NATS .NET client v2 supports .NET 6+ with an async-first API. Notably, .NET Aspire has official integration for NATS:
// Program.cs — Aspire AppHost
var nats = builder.AddNats("nats")
.WithJetStream();
var orderService = builder.AddProject<Projects.OrderService>()
.WithReference(nats);
// OrderService — DI registration
builder.AddNatsClient("nats");
// Usage in service
public class OrderProcessor(INatsConnection nats)
{
public async Task ProcessAsync(CancellationToken ct)
{
var js = nats.CreateJetStreamContext();
var consumer = await js.GetConsumerAsync("ORDERS", "processor", ct);
await foreach (var msg in consumer.ConsumeAsync<OrderEvent>(cancellationToken: ct))
{
try
{
await HandleOrder(msg.Data);
await msg.AckAsync(cancellationToken: ct);
}
catch (Exception ex)
{
await msg.NakAsync(cancellationToken: ct); // Redeliver
}
}
}
}
Real-World Pattern: Saga with NATS JetStream
Distributed transactions across multiple microservices using the Choreography Saga pattern with NATS:
sequenceDiagram
participant OS as Order Service
participant N as NATS JetStream
participant PS as Payment Service
participant IS as Inventory Service
participant NS as Notification Service
OS->>N: orders.created
N->>PS: orders.created (Consumer Group)
PS->>N: payments.completed
N->>IS: payments.completed
IS->>N: inventory.reserved
N->>NS: inventory.reserved
NS->>N: notification.sent
Note over PS,N: If payment fails
PS->>N: payments.failed
N->>OS: payments.failed (Compensate)
OS->>N: orders.cancelled
style N fill:#e94560,stroke:#fff,color:#fff
Choreography Saga pattern with NATS JetStream as the event backbone
// Stream configuration for Saga
var streamConfig = new StreamConfig("SAGA", subjects: new[]
{
"orders.>", "payments.>", "inventory.>", "notifications.>"
})
{
Retention = StreamConfigRetention.Interest, // Delete when all consumers ack
MaxAge = TimeSpan.FromHours(24),
Replicas = 3 // High availability
};
await js.CreateStreamAsync(streamConfig);
Monitoring and Observability
NATS Server exposes metrics via HTTP endpoints /varz, /connz, /subsz, easily integrable with Prometheus/Grafana. Additionally, nats-top provides real-time monitoring similar to Unix top:
# Monitoring endpoints
curl http://localhost:8222/varz # Server stats
curl http://localhost:8222/jsz # JetStream stats
curl http://localhost:8222/connz # Connection info
# Real-time monitoring
nats-top -s msgs_per_sec
When Should You Choose NATS?
Choose NATS JetStream when
1. You need a versatile messaging system (pub/sub + request/reply + streaming + KV) without operating multiple separate systems.
2. Sub-millisecond latency is a hard requirement.
3. You have edge/IoT deployments that need offline operation (Leaf Nodes).
4. Small team wanting operational simplicity — NATS runs reliably with near-zero maintenance.
5. Microservices need a request/reply pattern (Kafka and RabbitMQ don't natively support this well).
Consider Kafka instead when
1. Throughput above 500K msg/s with persistence is a hard requirement.
2. You need complex stream processing (Kafka Streams, ksqlDB).
3. Large-scale log aggregation / data pipelines with long-term retention (terabytes).
4. Ecosystem maturity: Schema Registry, Kafka Connect, hundreds of ready-made connectors.
Conclusion
NATS JetStream has proven that a messaging system doesn't have to be complex to be powerful. With a footprint of just ~20MB RAM, sub-millisecond latency, and the ability to deploy from cloud to edge on a single platform, NATS deserves serious consideration for any microservices architecture — especially when teams value operational simplicity. NATS Server 2.12 (April 2026) continues to improve atomic batch publishing and JetStream performance, solidifying NATS's position in the messaging platform race.
References
Vite 8 + Rolldown: The Unified Rust Bundler That Changes JavaScript Builds
WebRTC — Building Peer-to-Peer Video Call Architecture in the Browser
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.