Sizing & resources
Topologies decided the shape: a three-node cluster called east
(n1-east, n2-east, n3-east) carrying the R3 ORDERS stream. This
chapter runs that shape for real, and running it starts with the question
of how much of each resource this cluster needs.
A node spends a small, fixed set of resources, the JetStream defaults are knowable numbers, and the account limits the server enforces are readable with one command. This page turns those into a baseline for the ORDERS workload.
You'll learn two things here: the four resources a node spends (and their JetStream defaults), and how account limits count R3 replication against the storage ceiling.
The four resources a node spends
Every NATS node spends the same four resources. Once you size each one, you've sized the node.
CPU handles moving messages. Core NATS routing is cheap; TLS handshakes and JetStream replication are where cycles go. There's no hard CPU limit to set, so the rule is headroom: overprovision CPU by 20–30% above steady state so a node has cycles spare for a rebalance when a peer leaves.
Memory holds connections, subscriptions, and (for memory-storage
streams) message data. The ORDERS stream uses file storage, so its
messages live on disk, not in RAM. The order-svc publisher and the
warehouse, notifications, and analytics subscribers are light
clients; a few hundred connections fit comfortably in a few hundred
megabytes. Budget order-svc at roughly 128 MiB.
Disk holds file-storage streams. This is the resource the ORDERS stream actually spends, and the one most likely to run out. We'll size it below.
File descriptors (FDs) are the per-process limit on open files and
sockets. Connections, routes, and streams each consume FDs. JetStream
spends roughly two FDs per stream. On a small cluster the
default per-process limit is plenty; on a large one it isn't, which is
why the hardened service unit on the hardening
page raises it to LimitNOFILE=800000.
The JetStream defaults
When JetStream can't read the system's real limits, it falls back to two
defaults worth memorizing: memory storage 256 MB and file storage
1 TB. When it can read real system RAM, the server sizes memory
storage to a fraction of that instead, but the file-storage default stays
at 1 TB unless you set max_file_store lower.
That 1 TB default is a ceiling, not a reservation: the node only writes
what the streams actually store. But it causes a problem in a container.
The node will accept writes up to 1 TB even if the volume mounted under it
is 10 GiB, and the publish that crosses the real disk boundary fails. Set
max_file_store to match the volume:
# n1-east.conf — pin JetStream storage to the real volume size
jetstream {
store_dir: "/var/lib/nats/jetstream"
max_memory_store: 256MB
max_file_store: 10GB
}
The ORDERS stream is an R3 file stream. At R3 it keeps three copies, one
per node, so each node stores the full stream once. A 10 GiB
max_file_store per node leaves ample room for an ORDERS stream sized to
fit the default 10 GiB volume the Kubernetes chapter provisions.
The full set of JetStream limit keys is documented in
Reference → Configuration. We only cover
max_memory_store and max_file_store here.
Account limits and how replication counts
The server config sizes the node, while account limits size the
tenant. The ORDERS account has its own ceilings (MaxMemory,
MaxStore, MaxStreams, MaxConsumers), and the server enforces them
no matter how much disk the node has. Read them live before you size:
- CLI
#!/bin/bash
# Read the LIVE account limits for the ORDERS account before you size anything.
# This is the source of truth: it shows the tier and the ceilings the server
# will actually enforce, not what you hope the config says.
#
# Connect as order-svc in the ORDERS account, then ask the server.
nats account info \
--server tls://nats.acme.internal:4222 \
--creds /etc/nats/creds/order-svc.creds
# Look for these rows in the output:
# Memory: the MaxMemory ceiling for the account
# Storage: the MaxStore ceiling (file storage) for the account
# Streams: MaxStreams — how many streams the account may create
# Consumers: MaxConsumers — how many consumers per stream
#
# Sizing rule: on an UN-tiered account an R3 stream counts as
# replicas x bytes against MaxStore, so a 10 GiB ORDERS stream at R3
# spends 30 GiB of the account's storage limit. A tiered account bakes
# replication into the tier, so the number you see is the usable bytes.
One subtlety in that output decides your storage math: how replication
counts against MaxStore. There are two cases.
On an un-tiered account, an R3 stream counts as replicas × bytes.
A 10 GiB ORDERS stream at R3 spends 30 GiB of the account's
MaxStore, because the limit measures total bytes stored across all
replicas. Forget the multiplier and the third replica fails to place
when the account hits its ceiling.
On a tiered account, replication is baked into the tier. The bytes the limit reports are the usable bytes: the R3 multiplier is already accounted for, so a 10 GiB tier holds a 10 GiB R3 stream.
The durability R3 buys (surviving the loss of a node) is the JetStream chapter's subject, covered on Surviving node loss. Here you only need the cost: on an un-tiered account, three copies cost three times the bytes.
Connection, subscription, and payload limits
Three more limits round out the node, and one command reads them all:
- CLI
#!/bin/bash
# Read the LIVE server limits for one node of the east cluster.
# These are per-server ceilings: payload size, connection cap, and the
# JetStream memory/store limits the node was started with.
#
# Point at one node (here n1-east) and ask it about itself.
nats server info n1-east \
--server tls://nats.acme.internal:4222 \
--creds /etc/nats/creds/order-svc.creds
# Look for these in the output:
# Max Payload: max_payload (default 1.0 MiB) — the largest single message
# Max Connections: max_connections (default unlimited)
# JetStream: Max Memory and Max Storage configured on this node
#
# Sizing rule: max_payload must be <= max_pending. Keep max_pending at
# >= 10x your peak message size so a burst of large orders does not stall
# the connection. The ORDERS payload is well under 1 KiB, so the 1 MiB
# default has ample headroom here.
max_connections caps how many clients a node accepts (default
unlimited; it's reloadable, and overflow disconnects immediately).
max_subscriptions caps subscriptions per connection (default
unlimited). max_payload caps a single message at 1 MB by default,
and it must stay ≤ max_pending. The ORDERS payload is well under a
kilobyte, so the defaults have room to spare:
{"order_id":"ord_8w2k","customer":"acme-co","total_cents":4200,"ts":"2026-05-22T10:14:22Z"}
The full set of connection and payload keys is documented in Reference → Configuration. We only name the ones a sizing baseline needs here.
Pitfalls
A few sizing mistakes only surface in production under load, where they're expensive to fix. Each is scoped to this page's two concepts: the node's resources and the account's limits.
Setting max_file_store beyond the real disk. The 1 TB default (or
any value larger than the mounted volume) lets the node accept writes it
can't store. The failure is a publish error mid-stream, not a startup
warning. Set max_file_store to the volume size, test small with a
10GB value, and watch the real disk with df -h. Don't trust the
config number over what the device reports.
max_payload larger than max_pending. If max_payload exceeds
max_pending, the server refuses to start. Keep max_pending at
≥ 10× peak message size so a burst of large messages can't stall a
connection. Don't raise max_payload in isolation.
File-descriptor exhaustion on a big cluster. Each stream costs about
two FDs, and routes and gossip add more. On a large cluster the default
per-process FD limit runs out, and the symptom is connection refusals
that look like a network fault. Raise the limit (ulimit -n 800000)
before the process starts; the hardened unit on the
hardening page does exactly this.
JWT account limits on operator-mode clusters. Operator-mode accounts
enforce their limits through the account JWT; pre-v2.10 servers didn't
validate those limits at runtime. Upgrade an operator-mode cluster
atomically (all nodes together to v2.10+, never a rolling upgrade) so
one node never enforces a limit that another still ignores. Read the
active tier with nats account info before you size, so you plan against
the limits the upgraded cluster will actually apply.
The runnable fix for all four is the same first step: read the live limits before you size, so the numbers you plan against are the numbers the server enforces.
- CLI
#!/bin/bash
# Read the LIVE account limits for the ORDERS account before you size anything.
# This is the source of truth: it shows the tier and the ceilings the server
# will actually enforce, not what you hope the config says.
#
# Connect as order-svc in the ORDERS account, then ask the server.
nats account info \
--server tls://nats.acme.internal:4222 \
--creds /etc/nats/creds/order-svc.creds
# Look for these rows in the output:
# Memory: the MaxMemory ceiling for the account
# Storage: the MaxStore ceiling (file storage) for the account
# Streams: MaxStreams — how many streams the account may create
# Consumers: MaxConsumers — how many consumers per stream
#
# Sizing rule: on an UN-tiered account an R3 stream counts as
# replicas x bytes against MaxStore, so a 10 GiB ORDERS stream at R3
# spends 30 GiB of the account's storage limit. A tiered account bakes
# replication into the tier, so the number you see is the usable bytes.
Where you are
You now have a sizing baseline for the ORDERS cluster:
- The four resources a node spends: CPU (20–30% headroom), memory
(
order-svc~128 MiB), disk (max_file_storepinned to the volume), and FDs (two per stream). - The JetStream defaults named (memory 256 MB, file storage 1 TB) and
max_file_storeset to the real 10 GiB volume. - The account limits read live with
nats account info, and the rule that an un-tiered R3 stream costsreplicas × bytes.
The ORDERS R3 file stream fits the default 10 GiB volume, and order-svc
fits in ~128 MiB. That baseline is what the next page deploys.
What's next
With the resources sized, the next page stands the cluster up: the NATS
Helm chart, the StatefulSet that maps nats-0..2 to n1-east..n3-east,
and the NACK controller that declares the ORDERS stream as a CRD.
Continue to Kubernetes.
See also
- Reference → Configuration — every JetStream limit key and its default.
- Reference → max_payload — the payload and pending limits in full.
- Surviving node loss — what R3 durability buys for the bytes it costs.