Learn NATS - JetStream Performance
Series/Learn NATS/Episode 16
Episode 16 of 23

Learn NATS - JetStream Performance

This episode covers JetStream performance: high-throughput publishing in version 2.14+, batching for bulk publishing, server-side message scheduling, scaling with replication and partition across subjects, storage tuning, and benchmarks toward millions of messages per second.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

NATS is known as the fastest messaging system, but speed without the right techniques is still wasted. Episode 16 opens the JetStream performance toolbox: how to publish millions of messages per second without overloading the server, and how to design streams so they stay fast as data grows.

We'll cover high-throughput publishing in version 2.14+, batching, server-side message scheduling, scaling via replication and partitioning, storage tuning, and real benchmarks.

High-Throughput Publishing

What's New in 2.14+

Since NATS Server 2.14, publishing to JetStream has a first-class high-throughput path: messages are buffered on the server and written to disk in groups, not one by one. This removes the per-message fsync bottleneck.

Server with performance configuration
nats-server -c nats.conf --jetstream --store_dir /data/js

The nats-server --jetstream --store_dir /data/js command uses fast SSD disk storage. For high throughput, make sure the store directory sits on a fast local disk, not network storage.

Using Async Publishing

On the client side, use asynchronous publishing with periodic flushing:

Async publishing in Go
nc, _ := nats.Connect("nats://localhost:4222")
js, _ := nc.JetStream()
 
for i := 0; i < 100000; i++ {
    js.PublishAsync("orders.created", []byte("data"))
}
select {
case <-js.PublishAsyncComplete():
case <-time.After(30 * time.Second):
}

js.PublishAsync("orders.created", []byte("data")) sends without waiting for a per-message ack, and js.PublishAsyncComplete() waits until everything is sent. Async publishing lets thousands of messages flow through a single pipeline without a round-trip per message.

Batching: Combining Many Publishes

Batch per Session

Batching reduces network overhead by sending many messages at once:

PythonBatch publishing with nats-py
async with await js.publish_batch([
    js.create_msg("orders.created", b"pesan-1"),
    js.create_msg("orders.created", b"pesan-2"),
    js.create_msg("orders.created", b"pesan-3"),
]) as batch:
    await batch.wait_for_all(timeout=30)

js.publish_batch([...]) sends many messages as one operation. For data pipelines, batching drastically improves throughput compared to publishing one at a time.

Batching Practical Rules

  • Batches of 100-1000 messages give the best balance.
  • Don't make batches so large they exceed the maximum message limit.
  • Always handle batch errors: if one fails, know which message was lost.

Server-Side Message Scheduling

Server-Controlled Delivery Flow

Version 2.14 added server-side message scheduling: the server controls when consumers receive messages, so many consumers can share one stream without competing for bandwidth.

Consumer with flow control
nats consumer add ORDERS PROCESSOR --push --flow-control --ack explicit

The --flow-control flag enables server-side flow control. The server slows down delivery when a consumer can't keep up, preventing overload that leads to pending backlog.

Info

Flow control and heartbeat work together: the server sends heartbeats to make sure the connection is alive and delays messages if a consumer is full. Enable both for push consumers in production.

Scaling JetStream

Replication and Partitioning

For higher throughput, divide the load:

  • Replication adds copies for resilience, not throughput.
  • Partitioning across subjects: split one large subject into several tenant-based streams.
Separate streams per region
nats stream add ORDERS_EU --subjects "orders.eu.>"
nats stream add ORDERS_ASIA --subjects "orders.asia.>"

The nats stream add ORDERS_EU --subjects "orders.eu.>" command partitions data per region. Each stream is managed and scaled on its own — this architecture is called subject-based sharding.

Storage Tuning

A few tunings with big impact:

  • NVMe SSD for store_dir is much faster than HDD.
  • Choose file storage for durability, memory for maximum speed.
  • Adjust max_age and max_bytes so streams don't grow out of control.
Stream with storage tuning
storage: file
max_age: 7d
max_bytes: 4GiB
replicas: 3
compression: true

The compression: true block enables message compression to save disk. Combining limits and compression keeps write performance stable.

Real Benchmarks

Measuring Throughput

NATS provides the nats bench benchmarking tool:

Benchmark publish/subscribe
nats bench --pub 10 --sub 10 --size 256 --msgs 1000000 benchmark.subject

The nats bench --pub 10 --sub 10 --size 256 command runs 10 publishers and 10 subscribers sending 1 million 256-byte messages. The output shows messages per second and latency. NATS often shows millions of messages per second in benchmarks like this — depending on hardware and configuration.

Reading Benchmark Results

Example benchmark results
publish: 2,400,000 msg/sec
deliver: 2,350,000 msg/sec
latency: p50 1.2ms, p99 3.8ms

The publish: 2,400,000 msg/sec result above shows millions of messages per second throughput with millisecond latency. Remember: benchmarks are guidance, not promises — always test with your real workload patterns.

Conclusion

Episode 16 optimized JetStream: high-throughput publishing in version 2.14 with server-side buffering, async publishing and batching on the client, server-side message scheduling with flow control, scaling via partitioning across subjects and storage tuning, and benchmarks to measure real capability.

Key takeaways:

  • High-throughput publishing in 2.14+ writes messages to disk in groups.
  • Async publishing and batching improve throughput without a per-message round-trip.
  • Flow control lets the server adjust delivery speed.
  • Replication for resilience; partitioning across subjects for throughput.
  • SSD, size limits, and compression keep write performance stable.
  • nats bench measures throughput and latency as guidance.

In episode 17 next, we'll discuss NATS on Kubernetes — deploying with Helm charts and nats-operator, StatefulSets for JetStream with PVC-backed persistence, then KEDA NATS JetStream scaler integration for event-driven autoscaling, Argo workflows, and service meshes. Your NATS is ready to go cloud-native.

Learn NATS - JetStream Performance | Learn NATS