The Default Linux Ceiling for High-Concurrency WebSockets
Modern real-time systems—such as conversational voice agents, live trading terminals, telemetry ingestors, and collaborative canvases—rely on persistent WebSockets and HTTP/2 connections. While a standard REST API opens a socket, exchanges a request/response in 50ms, and closes the connection, a WebSocket client holds its TCP socket open for minutes or hours.
When you scale a single Linux VPS beyond 10,000 concurrent persistent connections, standard distributions (Ubuntu, Debian, AlmaLinux) hit hard operating system ceilings. Incoming connection attempts begin timing out, Nginx reports 111: Connection refused, and kernel dmesg logs show ominous alerts: TCP: possible SYN flooding on port 443. Sending cookies. This is not a hardware CPU shortage; it is the default Linux kernel TCP/IP stack rejecting traffic due to conservative buffer sizes and queue limits.
1. Anatomy of the TCP Connection Lifecycle Under Load
Before a WebSocket connection upgrades from HTTP, it must complete the standard TCP three-way handshake (SYN → SYN-ACK → ACK). Under high concurrent arrival rates, connection handshakes pass through two kernel queues:
| Kernel Queue / Parameter | Default Linux Value | Production 100k Value | Failure Mode If Undersized |
|---|---|---|---|
net.ipv4.tcp_max_syn_backlog |
128 - 512 | 65,535 | SYN queue overflows; new handshakes silently dropped |
net.core.somaxconn |
128 - 4096 | 65,535 | Accept queue overflows; Gunicorn/Node.js cannot accept connections fast enough |
net.ipv4.tcp_fin_timeout |
60s | 15s | Sockets linger in TIME_WAIT for a full minute, exhausting ephemeral ports |
net.ipv4.ip_local_port_range |
32768 60999 | 10240 65535 | Out of outbound ephemeral ports when reverse proxying |
fs.file-max |
~100,000 | 2,097,152 | Too many open files OS error across all processes |
2. Production `sysctl.conf` Configuration for 100k Connections
Apply these hardened kernel parameters to /etc/sysctl.d/99-highconcurrency.conf to scale network socket capacity and optimize memory consumption per connection:
# /etc/sysctl.d/99-highconcurrency.conf
# ==============================================================================
# Linux Kernel TCP/IP Hardening for 100,000+ Concurrent Sockets
# ==============================================================================
# 1. Expand Connection Handshake Queues
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.core.netdev_max_backlog = 65535
# 2. Ephemeral Port Range Expansion (Provides ~55,000 outbound ports)
net.ipv4.ip_local_port_range = 10240 65535
# 3. Aggressive TIME_WAIT Socket Recycling
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
# 4. Memory Buffer Tuning per Socket (Enables 100k sockets to fit in 4GB RAM)
# Min, Default, Max buffer allocations in bytes
net.ipv4.tcp_rmem = 4096 87380 4194304
net.ipv4.tcp_wmem = 4096 65536 4194304
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
# 5. TCP Keepalive Optimization (Detect dead mobile connections in 60s instead of 2 hours)
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 15
net.ipv4.tcp_keepalive_probes = 5
# 6. Global File Descriptor Limits
fs.file-max = 2097152
fs.nr_open = 2097152
Activate the configuration immediately without rebooting:
sudo sysctl -p /etc/sysctl.d/99-highconcurrency.conf
3. File Descriptor & Process Limits (`limits.conf`)
In Unix, every socket is represented as a file descriptor. Even if the kernel allows 2 million files globally, user processes are constrained by system security limits. Update /etc/security/limits.conf and your systemd unit service files:
# /etc/security/limits.conf
* soft nofile 1048576
* hard nofile 1048576
root soft nofile 1048576
root hard nofile 1048576
www-data soft nofile 1048576
www-data hard nofile 1048576
For services managed by systemd (Nginx, Gunicorn, Daphne), ensure the unit file explicitly declares LimitNOFILE:
[Service]
LimitNOFILE=1048576
LimitNPROC=512000
4. Nginx Worker & Event Loop Sizing
Finally, align Nginx with your kernel's enlarged epoll capabilities in /etc/nginx/nginx.conf:
user www-data;
worker_processes auto;
worker_rlimit_nofile 1048576;
events {
worker_connections 65535;
use epoll;
multi_accept on;
}
http {
# Keepalive timeouts for persistent WebSocket proxying
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
To inspect active connections and verify your limits live on production, see our companion checklist on Production Linux VPS Hardening.
For related production architectures and system implementations, explore these companion guides:
- Deploying Real-Time WebSockets & Voice AI — Size upstream persistent WebSocket connection pools and Daphne workers for persistent voice streams.
- Production Linux VPS Hardening Checklist — Apply core operating system hardening and file descriptor limit expansion across production nodes.
- Reverse Proxy Architecture with Nginx — Balance TCP buffer sizing with edge HTTP/2 multiplexing for optimal edge throughput.
Production Engineering Takeaways
- Keepalive matters: Dropping default TCP keepalive from 7200s to 300s prevents zombie connections from severed mobile devices from consuming RAM indefinitely.
- Buffer sizing dictates scale: Keeping default socket buffers at 64KB–87KB allows 100,000 idle WebSockets to reside comfortably in ~6GB of system RAM.
- Monitor queue drops: Use
netstat -s | grep -i listento verify that listen queue overflows remain at zero during traffic spikes.