New clients cannot connect to your NATS server. Existing connections keep working. Client logs show the protocol error -ERR 'Maximum Connections Exceeded' and then the connection closes. The server itself looks healthy: /healthz returns ok, messages still flow for connected clients, and nothing crashed.
This is the max_connections wall. The server has reached its configured connection limit and is rejecting every new connection during the handshake. There is no queueing and no graceful degradation. One slot short of the limit, everything works. At the limit, every new client is turned away.
The default limit is 65536, but many deployments hit the wall much earlier because an operator set a lower value, an account-level limit subdivides the global cap, or the OS file descriptor ceiling runs out before NATS ever gets there. This guide covers how the limit works, how to tell which ceiling you actually hit, and how to find the clients consuming the slots.
What this means
NATS tracks active client connections per server and compares the count to max_connections on every new connection attempt. When the count reaches the limit, the server accepts the TCP connection, sends -ERR 'Maximum Connections Exceeded' during the handshake, and closes the socket. From the client’s perspective the connection fails immediately; everything already connected is unaffected.
Two limits can reject a connection independently:
- Server-level
max_connections. The global cap for the server. Default 65536. - Account-level connection limits. Accounts can carry their own connection limit. A connection is rejected if either the server-level limit or the account-level limit is reached, so one account can hit its wall while the server has thousands of free slots.
There is also a third, silent ceiling: the OS file descriptor limit. Every connection consumes one file descriptor, plus routes, gateways, leaf nodes, JetStream store files, and listener sockets. If ulimit -n for the nats-server process is lower than max_connections, the OS limit bites first and the symptom is similar: new connections fail while existing ones work. This is a cliff-edge failure with no graceful degradation, and default ulimit -n values (often 1024) are catastrophically low for a busy NATS server.
flowchart LR
C[New client TCP connect] --> D{Under server and account limits?}
D -- yes --> OK[Handshake completes, client connected]
D -- no --> ERR["-ERR Maximum Connections Exceeded, socket closed"]
F[Connection leak or reconnect storm] -. consumes slots .-> D
G[OS ulimit -n exhausted] -. blocks accept .-> ERROne subtlety on what counts toward the limit: routes and gateways are internal inter-server connections, and leaf node connections carry multiplexed edge traffic. Whether each type counts against max_connections differs by connection type . Regardless of how they count against max_connections, every one of them consumes file descriptors, so FD exhaustion arithmetic must include routes, gateways, and leaf nodes.
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Connection leak in a client application | connections climbs monotonically over hours or days; total_connections grows faster than expected | Compare connections vs total_connections on /varz; inspect /connz for old, idle connections |
| Reconnection storm | Sharp spike in connections after a network event, deploy, or server restart; CPU spike from TLS handshakes | total_connections delta over the last minutes; correlate with uptime and recent network events |
| Client pool misconfiguration | Connection count is a suspicious round multiple of the number of app instances | Count connections per client IP or name in /connz |
| Account-level limit reached | Only one account’s clients are rejected; other accounts connect fine | Per-account stats via /accstatz against the account’s configured limit |
| OS file descriptor exhaustion | Rejections start well below max_connections; accept errors in server logs | cat /proc/$(pidof nats-server)/limits and count open FDs |
| Crashed clients holding stale slots | Connections with no traffic that never cleanly closed | /varz stale_connections field |
Quick checks
All of these are read-only. The monitoring port defaults to 8222.
# Current connections, lifetime total, and configured limit
curl -s http://localhost:8222/varz | jq '{active: .connections, total: .total_connections, max: .max_connections}'
# Utilization as a percentage of the configured limit
curl -s http://localhost:8222/varz | jq '{utilization_pct: ((.connections / .max_connections) * 100)}'
# Stale connections and stalled clients (dead or distressed slots)
curl -s http://localhost:8222/varz | jq '{stale_connections, stalled_clients}'
# Who is holding connections: group by client IP
curl -s "http://localhost:8222/connz?limit=1000" | jq '[.connections[].ip] | group_by(.) | map({ip: .[0], count: length}) | sort_by(-.count) | .[:10]'
# OS file descriptor ceiling for the running server process
cat /proc/$(pidof nats-server)/limits | grep -i "open files"
# Approximate current FD usage of the process
ls /proc/$(pidof nats-server)/fd | wc -l
# Look for accept failures in the server log (adjust path to your logging setup)
grep -i "accept" /var/log/nats/nats-server.log | tail -20
# Or, under systemd:
journalctl -u nats-server --since "30 min ago" | grep -i "accept"
A note on /connz at scale: on servers with many thousands of connections, full /connz scrapes are expensive. Use limit and offset parameters and avoid tight polling loops against it during an incident.
How to diagnose it
Confirm which wall you hit. Pull
/varzand compareconnectionstomax_connections. Ifconnectionsequalsmax_connections, you hit the NATS limit. Ifconnectionsis well belowmax_connectionsbut new clients are still rejected, suspect the OS file descriptor ceiling or an account-level limit.Rule in or out the FD ceiling. Compare the process
Max open fileslimit against the count of open FDs. The FD budget includes routes, gateways, leaf nodes, JetStream store files, and listener sockets, not just client connections. If the FD count is at or near the limit, this is your wall regardless of whatmax_connectionssays. Production NATS servers should run with at least 65536 open files; many teams hit this cliff on their first traffic spike.Determine whether growth is gradual or sudden. Compute the delta of
total_connectionsover a few minutes. A fast-climbingtotal_connectionswith a stable or spikingconnectionsmeans churn: clients connecting and disconnecting rapidly, which points to a reconnection storm or a crash-looping client that reconnects on every restart. Slow, steady growth inconnectionswith modest churn points to a leak.Identify who holds the slots. Use
/connzgrouped by IP, client name, or account to find concentration. A leak usually shows as many long-lived connections from one application or host. A storm shows as many fresh connections with short uptime.Check for stale slots. Non-zero
stale_connectionsin/varzmeans clients that failed ping/pong health checks but still occupy slots. Clients that crash without a clean close leave the TCP connection open until the server’s stale detection fires, so a fleet of crashing clients can hold a large number of dead slots during the detection window.Check account-level limits if only some clients fail. If clients in one account are rejected while others connect normally, compare that account’s usage from
/accstatzagainst its configured connection limit. Per-account limits are cluster-global, not per-server .Correlate with the trigger. A spike that lines up with a deploy, a network partition healing, or a server restart is a reconnection storm. A slow climb with no external event is a leak. Check
uptimeon/varzand your deploy timeline before blaming the clients.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
connections / max_connections ratio | The only correct way to alert on this wall; absolute counts break across deployment sizes | >80% warn, >95% critical |
total_connections rate (churn) | Reveals reconnect storms and crash-looping clients that a stable connections count hides | Rapid growth while connections is flat or spiking |
Process FD usage vs ulimit -n | The OS ceiling often bites before max_connections does | FD count >70% of the open-files limit |
stale_connections | Dead clients occupying real slots | Any non-zero value sustained >5 minutes |
slow_consumers rate | Slow consumer disconnects trigger reconnect cycles that churn connection slots | Positive rate alongside rising total_connections |
mem (RSS) | Each connection costs memory for buffers and goroutines; growth foreshadows the wall | Monotonic growth tracking connection growth |
Per-account connections (/accstatz) | Account limits reject before the server limit does | Account usage >80% of its configured limit |
Alert on the ratio connections / max_connections, never an absolute count. “Alert when connections > 1000” breaks as soon as the limit or the environment changes; “alert when connections exceed 80% of max_connections” survives both. Apply the same ratio thinking to the FD ceiling.
Fixes
Stop the bleeding: free or reclaim slots
If clients are leaking, restarting the offending application releases its slots immediately. This is disruptive to that application but does not touch the NATS server or its healthy clients. Do not restart the NATS server to fix a client-side leak; the slots refill as soon as the leaking clients reconnect.
If the pressure is a reconnection storm, the server usually recovers on its own once clients settle. Check that the storm is decaying (total_connections rate falling) before intervening. If it is cyclical, the underlying cause (flapping load balancer, DNS failure, network device) is still active and that is where to focus.
Raise the limit, carefully
max_connections is a configuration value. Increasing it is legitimate when the current value is simply too small for real load, but it only moves the wall. Before raising it:
- Verify the FD ceiling first. Raising
max_connectionsabove the processulimit -nis pointless and gives false confidence. Set the open-files limit high enough to covermax_connectionsplus routes, gateways, leaf nodes, JetStream files, and headroom. - Budget the memory. Every connection carries per-connection buffers and goroutines. A large limit on a small host trades connection rejections for memory pressure.
NATS supports runtime configuration reload, and max_connections is among the reloadable settings, so you can raise it without disconnecting existing clients . Test the reload path in staging before relying on it during an incident.
Fix the leak
The durable fix for a leak is in the client: ensure connections are closed on shutdown and not recreated per operation. Common patterns are creating a new connection per request instead of reusing one, or losing the reference to a connection without closing it. Use the /connz inventory you gathered during diagnosis to confirm the fix: connection count per host should return to the expected pool size.
Fix the pool misconfiguration
If each application instance opens far more connections than intended, correct the pool size in the client configuration. Connection pooling defaults in some frameworks are generous; a few hundred instances times a large default pool can fill a server that looked comfortably sized.
Prevention
- Alert on the ratio, early. Warning at >80% and critical at >95% of
connections / max_connectionsgives you hours or days of runway for a leak, and at least a fighting chance during a storm. Keep roughly 20% free slots as standing headroom to absorb reconnection bursts. - Monitor the FD ceiling as its own signal. Track process FD usage against
ulimit -nand alert well before exhaustion. Setulimit -nto at least 65536 for production servers, more if the connection budget demands it. - Watch churn, not just the count.
total_connectionsdelta relative to a stableconnectionscount is the earliest indicator of flapping clients and storm conditions. - Size account limits deliberately. If you use accounts, set per-account connection limits so one misbehaving tenant cannot consume the entire server budget.
- Test reconnection behavior. Client libraries reconnect automatically; verify that your clients use backoff and jitter so a healed partition does not produce a synchronized thundering herd. A storm can consume all free slots in seconds even when steady-state usage is tiny.
- Audit client shutdown paths. Clients that exit without closing connections leave slots occupied until stale detection cleans them up. Graceful close on shutdown is cheap insurance.
How Netdata helps
Netdata’s NATS collector polls the server’s HTTP monitoring endpoints and turns the raw counters into the signals this incident actually needs:
- Connection utilization as a ratio. Netdata charts
connectionsalongsidemax_connectionsfrom/varz, so you see saturation approaching as a percentage, not an absolute number you have to mentally compare. - Churn visibility.
total_connectionsis collected as a cumulative counter, so the rate view exposes reconnect storms and crash-looping clients even when the live connection count looks stable. - Leak detection over time. Per-second and historical retention makes the slow monotonic climb of a connection leak obvious, and lets you correlate the start of the climb with a deploy or config change.
- Correlation with the downstream signals. Slow consumers, memory, and throughput live on the same dashboard, so you can see whether connection churn is coming from slow-consumer disconnect cycles or from an upstream network event.
- Cluster-wide view. In a multi-server cluster, per-server connection charts show whether the wall is one overloaded node or a systemic growth trend across all nodes.
Related guides
- NATS monitoring checklist: the signals every production server needs
- NATS monitoring maturity model: from survival to expert
- NATS how it works in production: a mental model for operators
- NATS server not responding: healthz failing and the process down or hung
- NATS crash loop: unexpected uptime resets and repeated restarts
- NATS slow consumer detected: the write buffer overflowed and messages were dropped
- NATS slow consumer breakdown: clients vs routes vs gateways and blast radius
- NATS /healthz explained: js-server-only vs js-enabled-only vs the bare check






