Checks/SERVER_005

NATS JetStream Memory Pressure: What It Means and How to Fix It

Severity
Warning
Category
Saturation
Applies to
Server
Check ID
SERVER_005
Detection threshold
JetStream memory usage >= 90% of reserved (max_mem)

JetStream memory pressure means a NATS server’s memory-backed stream usage has reached or exceeded 90% of the reserved JetStream memory (max_mem). At this level, the server is approaching the hard limit where new writes to memory-backed streams will be rejected outright.

Why this matters

Memory-backed streams in NATS JetStream store all messages in RAM for the lowest possible read and write latency. The trade-off is that every byte of message data directly consumes server memory, bounded by the max_mem reservation configured for JetStream on that server. When usage crosses 90%, you are one traffic spike away from hitting the ceiling.

When a server exhausts its JetStream memory reservation, all publish operations to memory-backed streams on that server fail with a “no space” error. Unlike file-backed storage, there is no slow degradation — the transition from “working” to “rejecting writes” is binary and abrupt. Publishers receive errors, request-reply chains break, and any system that depends on memory-backed streams for low-latency data flow stops making progress.

The risk compounds in clustered environments. Memory-backed stream replicas consume memory on every server that hosts a replica. An R3 memory-backed stream consuming 500 MiB uses 500 MiB on three separate servers. If all three servers are running near their memory limit, a single burst of publishes can push multiple servers past their threshold simultaneously, causing a cluster-wide write failure for all memory-backed streams — not just the one that triggered the pressure.

Common causes

  • Memory-backed streams growing beyond expectations. A stream was created with storage: memory for low-latency access, but no max_bytes or max_age limit was set. Messages accumulate without bound until the server’s max_mem reservation is exhausted. This is the most common cause.

  • Traffic spike to memory-backed streams. A burst of publishes — batch imports, backfills, or incident-driven traffic — fills memory faster than retention policies can expire old messages. The stream’s retention limits may be correctly configured for steady-state, but inadequate for peak load.

  • Over-provisioned replica count. An R3 memory-backed stream consumes 3x the memory of an R1 stream for the same data. Over-replicating memory-backed streams when high availability isn’t required wastes the scarcest resource in the cluster.

  • Undersized max_mem reservation. The server’s JetStream memory reservation was set conservatively during initial deployment and hasn’t been increased as workloads grew. The reservation should reflect actual peak memory needs, not initial estimates.

  • Large deduplication windows on high-throughput streams. The deduplication map for Nats-Msg-Id headers is held in memory. A 2-minute dedup window at 50,000 msg/s with UUID-based message IDs can consume hundreds of megabytes of memory that counts against max_mem.

How to diagnose

Check JetStream memory usage per server

Terminal window
nats server report jetstream

Look at the MEM and MEM MAX columns. The ratio tells you how close each server is to its memory reservation. Any server above 90% triggers this check.

Identify which streams consume the most memory

Terminal window
nats stream report

Sort by memory usage to find the largest memory-backed streams. Pay attention to streams with storage: memory — file-backed streams don’t contribute to max_mem pressure.

Check individual stream configuration and state

Terminal window
nats stream info <stream_name>

Look at:

  • Storage: Memory vs File
  • Messages/Bytes: Current size
  • Limits: max_bytes, max_msgs, max_age — are they set?
  • Replicas: How many copies exist?
Terminal window
# Check the JetStream advisory subjects for resource pressure events
nats sub '$JS.EVENT.ADVISORY.>'

If you have Prometheus monitoring.

How to fix it

Immediate: free memory now

Purge non-critical memory-backed streams. If any memory-backed stream contains data that can be regenerated or isn’t time-sensitive, purge it:

Terminal window
nats stream purge <stream_name>

Set retention limits on unbounded streams. If a memory-backed stream has no limits configured, add them immediately:

Terminal window
nats stream edit <stream_name> --max-bytes 256MiB --max-age 1h

Short-term: right-size memory-backed streams

Convert large streams to file-backed storage. Most streams that use storage: memory don’t actually need sub-millisecond latency. File-backed streams with SSD storage provide single-digit millisecond latency — fast enough for the vast majority of workloads:

1
// When creating a new stream, use file storage unless
2
// you have a measured latency requirement for memory
3
js, _ := nc.JetStream()
4
_, err := js.AddStream(&nats.StreamConfig{
5
Name: "ORDERS",
6
Subjects: []string{"orders.>"},
7
Storage: nats.FileStorage, // not MemoryStorage
8
MaxBytes: 1 << 30, // 1 GiB
9
MaxAge: 24 * time.Hour,
10
})
1
import nats
2
from nats.js.api import StreamConfig, StorageType
3
4
nc = await nats.connect()
5
js = nc.jetstream()
6
7
await js.add_stream(StreamConfig(
8
name="ORDERS",
9
subjects=["orders.>"],
10
storage=StorageType.FILE, # not MEMORY
11
max_bytes=1 << 30, # 1 GiB
12
max_age=86400, # 24 hours
13
))

Reduce replica count on non-critical memory-backed streams. If a memory-backed stream is R3 but the data is ephemeral or easily regenerated, reduce to R1:

Terminal window
nats stream edit <stream_name> --replicas 1

Increase the max_mem reservation. If the server has physical memory headroom, increase the JetStream memory reservation in the server configuration:

1
jetstream {
2
max_mem: 4GiB # increased from 2GiB
3
}

Reload the configuration without restart:

Terminal window
nats-server --signal reload

Long-term: establish memory governance

Enforce max_bytes on all memory-backed streams. Use account-level JetStream limits to cap total memory usage per account, and require max_bytes on every stream:

1
accounts {
2
PRODUCTION {
3
jetstream {
4
max_mem: 1GiB
5
max_disk: 100GiB
6
}
7
}
8
}

Default to file storage. Adopt a team convention: memory-backed streams require explicit justification (measured latency requirements). Everything else uses file storage. This prevents accidental memory pressure from streams that don’t need the performance characteristics of RAM.

Monitor with Insights. Synadia Insights evaluates JetStream memory pressure every collection epoch across all servers. When pressure reaches 90%, this check fires as a warning. If memory continues climbing to the critical threshold, JETSTREAM_007 (JetStream Memory Utilization Critical) fires with critical severity — at that point, writes are failing or about to fail.

Frequently asked questions

What happens when JetStream memory usage hits 100% of max_mem?

All publish operations to memory-backed streams on that server are rejected with a “no space” error. Existing messages remain accessible for reading. The server itself continues operating — file-backed streams, core NATS, and cluster routes are unaffected. Only memory-backed stream writes fail.

Does max_mem include NATS server process memory?

No. max_mem is exclusively the JetStream memory reservation for stream data. The NATS server process itself uses additional memory for connection buffers, subscription routing tables, Raft state, and internal bookkeeping. Total server memory consumption is always higher than max_mem. Plan for at least 2x max_mem in available physical memory.

Should I use memory-backed streams at all?

Yes, but selectively. Memory-backed streams are ideal for high-frequency, low-latency data that is either ephemeral or easily regenerated — real-time pricing feeds, session state, caching layers. For anything where durability matters or message volume is unpredictable, file-backed storage is the safer default. SSD-backed file streams provide single-digit millisecond latency, which satisfies most workloads.

How do I set up alerting for JetStream memory pressure?

Monitor the ratio of used to reserved JetStream memory via the /jsz monitoring endpoint or Prometheus.

Alert at 85% to give yourself a buffer before the 90% warning threshold. Synadia Insights evaluates this automatically every epoch across your entire deployment.

Can I change a stream from memory to file storage without downtime?

Not in place — you cannot change the storage type of an existing stream. The migration path is: create a new file-backed stream, set up a mirror or source to replicate data from the old stream, switch publishers and consumers to the new stream, then delete the old memory-backed stream. This can be done with zero message loss using JetStream sourcing.

Proactive monitoring for NATS jetstream memory pressure with Synadia Insights

With 100+ always-on audit Checks from the NATS experts, Insights helps you find and fix problems before they become costly incidents.
No alert rules to write. No dashboards to maintain.

Start a 14-day Insights trial
Cancel