When a Consul server hits its file descriptor ceiling, every consumer that needs a new FD fails immediately: RPC connections are refused, DNS listeners stop accepting queries, xDS streams to Envoy sidecars cannot open, and outbound health-check connections fail. Gossip probes time out and the failure detection protocol marks otherwise healthy peers as suspect or failed. A healthy-looking cluster a minute ago is now generating cascading pages.
File descriptor exhaustion is not graceful degradation. Each socket, pipe, and file handle counts against a hard kernel limit. Once that limit is reached, every syscall that creates a new FD (accept, socket, open, pipe, epoll_create) returns EMFILE, regardless of available CPU, memory, or network capacity. A consul reload either fails outright (it needs FDs to read config and re-open listeners) or succeeds but leaves the server half-functional. Recovery requires raising the limit and restarting the process.
The default Linux ulimit -n of 1024 is catastrophically low for any Consul server. HashiCorp recommends a minimum of 65536 (LimitNOFILE=65536 in the systemd unit). Servers open FDs for every RPC client connection, gRPC stream, DNS listener, outbound health check, watch handler, and Raft peer connection. A cluster with a few hundred client agents and Connect enabled can legitimately sit at tens of thousands of FDs.
How it fails
The kernel bounds each process’s file descriptor table by a soft limit (what ulimit -n reports) and a hard limit (an upper bound the process can raise into). When the soft limit is hit, Go programs like Consul surface the errors as “too many open files” in logs and socket: too many open files in dial errors.
A server provisioned without explicit LimitNOFILE configuration will run for weeks at a few hundred FDs, then tip over when a deployment adds client agents, a Connect sidecar rollout begins, or a blocking-query leak accumulates past the threshold. Anything below 65536 on a server is a latent incident.
flowchart TD A[FD count hits ulimit] --> B[New RPC, DNS, xDS refused] A --> C[Outbound health checks fail] A --> D[Gossip probes time out] B --> E[Client agents cannot sync catalog] C --> F[Checks go stale] D --> G[Peers marked suspect or failed] E --> H[Silent catalog staleness] G --> I[False failure detection cascades] F --> I I --> J[Raft leader churn risk] J --> K[Write outage]
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Default ulimit still in effect | First “too many open files” page fires at a few hundred to a thousand FDs | /proc/$(pgrep consul)/limits |
| Connection or stream leak | FD count trends upward monotonically, never drops during quiet periods | consul_runtime_sys_fd_used over 24h |
| Blocking query or watch accumulation | Goroutine count climbs in lockstep with FDs | consul_runtime_num_goroutines vs FD count |
consul reload leak | Stepwise FD growth aligned with reload events | Count reloads vs FD deltas |
| consul-template idle HTTP connections | Steady FD growth on agents running many templates | lsof on the consul-template process |
| xDS stream cap (not FD) | Error mentions “xDS streams” not “open files” | Consul version and Connect topology |
| K8s cgroup limit hit | Container restarts or fails inside a node with a higher ulimit | /proc/<pid>/limits inside the container and the pod spec |
The last two rows are not FD exhaustion but produce symptoms operators frequently misread as FD exhaustion. The xDS stream cap introduced in Consul 1.14 (ResourceExhausted: this server has too many xDS streams open) is a per-server application-level limiter, not a kernel FD limit. The fix shipped in 1.15.0 (PR #15789) for locally-registered proxies. For consul-dataplane against external servers, the common root cause is a TLS SNI mismatch causing gRPC load-balancing failure, fixed by setting tlsServerName in the externalServers stanza.
Quick checks
Read-only and safe on a live server:
# Effective soft and hard limits.
# In Kubernetes, run from inside the container. `ulimit -n` from a host
# shell may report the node value, not the cgroup-imposed limit.
cat /proc/$(pgrep consul)/limits | grep "Max open files"
# Current open FD count
ls /proc/$(pgrep consul)/fd | wc -l
# FD usage and limit from Consul telemetry
curl -s http://127.0.0.1:8500/v1/agent/metrics | grep -E "consul_runtime_sys_fd_used|consul_runtime_sys_fd_limit"
# FD breakdown by type (IPv4, IPv6, sock, pipe, anon_inode)
lsof -p $(pgrep consul) -n | awk '{print $5}' | sort | uniq -c | sort -rn
# Established TCP connections to the RPC port (8300)
ss -tnp state established '( sport = :8300 )' | wc -l
# Canonical error in logs
journalctl -u consul --since '1 hour ago' | grep -i "too many open files"
# Goroutine count alongside FDs (correlation hints at a leak)
curl -s http://127.0.0.1:8500/v1/agent/metrics | grep consul_runtime_num_goroutines
How to diagnose it
Confirm the limit and current count. Pull
consul_runtime_sys_fd_usedandconsul_runtime_sys_fd_limitfrom telemetry, or read/proc/<pid>/fdand/proc/<pid>/limitsdirectly. Compute the ratio: PAGE above 90%, TICKET above 70%.Categorize the FDs. Run the
lsofbreakdown above. A highsockorIPv4count points at network connections (RPC, xDS, gossip). A highpipeoranon_inodecount suggests eventfds and epoll handles, often correlated with goroutine count.Determine whether this is a leak. Plot
consul_runtime_sys_fd_usedover 24 to 72 hours. Slow, non-reversing growth that does not correlate with cluster activity is a connection or stream leak. Stepwise growth aligned withconsul reloadevents points at the reload leak (issue #3018).Correlate with goroutines. If
consul_runtime_num_goroutinesclimbs in lockstep with FDs, the leak is in a code path that opens both a goroutine and a connection. Blocking queries and Connect xDS streams are the usual suspects.Check gossip and Raft side effects. Watch
consul_serf_lan_member_statusfor suspect entries andconsul_raft_last_contactfor upward drift. FD exhaustion often surfaces here first because gossip probes and Raft heartbeats fail to allocate sockets before the leader fails to commit.If Connect is enabled, distinguish FD exhaustion from the xDS stream cap. A
ResourceExhaustedgRPC error mentioning “xDS streams” is the 1.14+ stream limiter, not the kernel FD limit. Confirm by checking Consul version and proxy topology.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
consul_runtime_sys_fd_used | Absolute FD consumption | Monotonic growth over hours with no load change |
consul_runtime_sys_fd_limit | The ceiling the process will hit | Below 65536 on a server is misconfigured |
consul_runtime_sys_fd_used / fd_limit | Headroom ratio | PAGE above 90%, TICKET above 70% |
consul_runtime_num_goroutines | Proxy for concurrent connections and streams | Climbing in lockstep with FDs indicates a leak |
consul_xds_server_streams | Active xDS streams to Envoy sidecars | Unexpected growth signals reconnection churn |
consul_serf_lan_member_status | Gossip health | Spike in suspect members hints at FD pressure |
consul_raft_last_contact | Follower-to-leader reachability | Sustained upward drift risks election timeout |
consul_client_rpc_failed | Agent-to-server RPC health | Sustained non-zero on multiple agents means server is rejecting connections |
Fixes
Raise the limit (immediate relief)
Add LimitNOFILE=65536 to the [Service] section of the Consul systemd unit via a drop-in override:
systemctl edit consul
[Service]
LimitNOFILE=65536
systemctl daemon-reload
systemctl restart consul
Warning:
systemctl restart consulis disruptive. A restart of the leader causes a brief Raft election and write outage. If this is the leader, drain first or perform the restart during a maintenance window. The restart is the recovery action; raising the limit prevents recurrence.
In Kubernetes, raise the limit through the pod spec or container runtime. Check both the node-level ulimit and the cgroup-imposed limit, since ulimit -n inside a container can report the node value rather than the binding cgroup ceiling.
Stop the leak (longer term)
If FD growth is monotonic, the higher limit only buys time. Capture a goroutine dump during the incident:
curl -s http://127.0.0.1:8500/debug/pprof/goroutine?debug=1 > consul-goroutines.txt
Look for stack traces concentrated in RPC accept loops, blocking query handlers, or xDS stream management. The dominant stack identifies the leaking subsystem.
For consul reload-induced leaks, reduce reload frequency. Watch configurations accumulate handlers across reloads (issue #3018). Consider templating configurations at deploy time rather than relying on live reloads.
For consul-template, watch for idle HTTP connection accumulation (consul-template issue #591). A periodic bounce of consul-template, or upgrading to a version with the fix, addresses the slow leak.
Address the xDS stream cap separately
If the error mentions xDS streams rather than open files, raising the FD limit does not help. The relevant fixes:
- For Consul 1.14.x with locally-registered proxy services, upgrade to 1.15.0 or later, which disables the stream limiter for locally-registered proxies (PR #15789).
- For consul-dataplane against external servers, verify
tlsServerNamein theexternalServersstanza. A TLS SNI mismatch causes gRPC load-balancing failure and stream multiplication.
Prevention
- Set
LimitNOFILE=65536on every server. Anything lower is a misconfiguration that will fail under load. - Monitor the FD ratio with low thresholds. PAGE above 90%, TICKET above 70%.
- Trend FD usage over days. Slow monotonic growth is the leak signature. Catching it at 30% utilization is cheaper than catching it at 90%.
- Track goroutine count alongside FDs. They move together for connection-driven leaks.
- Check Kubernetes limits independently. The node ulimit is not the value that binds the pod.
- Limit
consul reloadfrequency. Each reload is a candidate leak source. - Validate Consul version behavior for Connect. The xDS stream limiter behavior changed in 1.14 and was refined in 1.15.
How Netdata helps
- Per-second
consul_runtime_sys_fd_usedandconsul_runtime_sys_fd_limitfrom Consul telemetry, surfaced as a ratio so the cliff edge is visible at a glance. - ML-based anomaly detection on monotonic FD growth catches leaks days before the kernel limit is hit. Threshold alerts at 70% and 90% provide operational tripwires.
- Correlation of FD utilization with
consul_runtime_num_goroutinesdistinguishes connection-driven leaks from file-handle or pipe leaks. - Per-process file descriptor charts, broken down by type, show whether growth is sockets, pipes, or anon_inode.
- Cross-signal correlation with
consul_serf_lan_member_status,consul_raft_last_contact, andconsul_client_rpc_failedshows gossip and Raft side effects in the same view. - Side-by-side comparison across all Consul servers makes it obvious when one server accumulates FDs faster than its peers.
Related guides
- Consul catalog bloat: too many services and checks slowing everything down
- Consul registration storm: catalog churn overwhelming Raft
- Consul anti-entropy not syncing: local agent state and the catalog drifting apart
- Consul client rpc failed: agents alive but the catalog is going stale
- Consul DeregisterCriticalServiceAfter: instances vanishing from the catalog
- Consul DNS latency high: slow lookups stalling connections and failovers
- Consul DNS SERVFAIL: service discovery is broken for your applications
- Consul stale DNS queries: the agent is answering from cache
- Consul on EBS: burst-credit exhaustion and the sudden latency cliff
- Consul gossip encryption key mismatch: a botched keyring rotation splits the pool
- Consul gossip flapping: nodes oscillating between alive, suspect, and failed
- Consul serf queue backlog: an agent falling behind on gossip






