Back to articles
Technology Insight

Deep Linux Kernel Tuning: Scaling Node.js VPS to 100,000 Concurrent WebSockets

May 30, 2026

Introduction: The Real-Time Scaling Challenge

In the era of real-time financial dashboards, collaborative tools, and instant messaging applications, maintaining thousands of persistent connections is a fundamental requirement. Node.js, built on the asynchronous V8 engine and utilizing an event-driven, non-blocking I/O model, is theoretically perfect for this workload. However, out-of-the-box Linux Virtual Private Servers (VPS) are tuned for general-purpose workloads, not the extreme network concurrency demanded by high-volume WebSockets.

When trying to push a standard Node.js server beyond a few thousand concurrent WebSocket connections, you will inevitably hit systemic barriers. Connections will drop, latency will spike, and the dreaded EMFILE: too many open files error will crash your process. To achieve the milestone of 100,000 concurrent WebSockets on a single VPS, we must look beyond application code and perform deep, architectural modifications to the Linux kernel itself. This guide provides an enterprise-ready roadmap to unlocking your server's true network capacity.

Understanding the Bottlenecks: Files and Memory

Before modifying configuration files, it is crucial to understand how Linux manages connections. In Unix-like operating systems, everything is a file. Every established TCP connection allocated to a WebSocket translates directly to an open file descriptor (FD). By default, standard Linux distributions limit individual processes to 1,024 file descriptors—a defensive threshold designed to prevent runaway resource consumption, but a crippling restriction for real-time applications.

Furthermore, each TCP connection requires memory for read and write buffers. Default kernel settings often allocate too much memory per socket, leading to Out-Of-Memory (OOM) panics long before reaching the 100k target. Scaling successfully requires balancing the max file limits while minimizing the memory footprint of each individual socket without sacrificing throughput.

Step 1: Raising the File Descriptor Limits

To allow Node.js to handle 100,000 concurrent connections, we must raise both the system-wide limits and the user-specific limits for file descriptors. This is handled via the /etc/security/limits.conf file and systemd configurations.

Modifying System-Wide Limits

First, open /etc/sysctl.conf and add the following line to define the maximum number of files the entire system can open:

fs.file-max = 2097152

Configuring User and Process Limits

Next, edit /etc/security/limits.conf to permit the user running the Node.js application (e.g., www-data or a dedicated node user) to allocate enough resources:

node    soft    nofile    200000
node    hard    nofile    200000

If you run your Node.js application using Systemd (which is standard practice for production deployments), these limits can be overridden by security sandboxing. You must explicitely configure your service file (e.g., /etc/systemd/system/node-app.service) to include:

[Service]
LimitNOFILE=200000

Apply these changes by reloading Systemd using systemctl daemon-reload and restarting your application service.

Step 2: Optimizing the Linux IP Stack (sysctl.conf)

The core of our optimization takes place within the network subsystem. By appending granular parameters to /etc/sysctl.conf, we alter how the kernel manages connection backlogs, reuse states, and timeouts.

Expanding the Port Range and Local Backlogs

When a server initiates or accepts massive amounts of connections, it can quickly exhaust available ephemeral ports. We expand this range to its safe maximum and increase the backlog queue to prevent incoming connection drops during traffic bursts:

  • net.ipv4.ip_local_port_range: Defines the range of local ports used for outbound connections or reverse proxies. Set this to 1024 65535.
  • net.core.somaxconn: The maximum backlog of established connections waiting to be accepted by the Node.js accept() loop. Increase this from 128 to 65535.
  • net.ipv4.tcp_max_syn_backlog: The maximum number of queued embryonic connections (syn-received state). Increase this to 65535 to withstand traffic spikes.

Fine-Tuning TCP Window Buffers for Low-Memory Footprint

By default, Linux allocates significant memory to each socket to maximize bandwidth throughput. Because WebSockets typically transmit frequent, small payloads rather than large continuous streams, we can aggressively scale down the allocated buffer sizes to conserve RAM:

# Format: min, default, max buffer sizes in bytes
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

Setting the minimum memory to 4KB ensures that idle connections consume negligible RAM, preserving system resources to easily sustain over 100,000 active states simultaneously.

Connection Reusing and Fast Timeouts

To avoid lingering sockets in the TIME_WAIT state occupying valuable slots, we modify the recycling behavior:

net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.core.netdev_max_backlog = 100000

Applying these adjustments requires executing the terminal command: sudo sysctl -p.

Step 3: Node.js Application-Level Adjustments

Tuning the Linux operating system provides the necessary environment, but the Node.js runtime must also be instructed to handle this scale. When using popular WebSocket libraries like ws or Socket.io, ensure the underlying http.Server instance handles errors elegantly and doesn't leak memory.

Leveraging Garbage Collection and Clustering

Node.js is single-threaded by nature. To maximize modern multi-core VPS environments, leverage the Cluster module or a process manager like PM2. Distributing 100,000 connections across 4 or 8 CPU cores drops the connection load per thread dramatically:

pm2 start server.js -i max

Additionally, monitor your V8 heap limits. You can explicitly set the maximum memory allocation using the flag --max-old-space-size=4096 to allow Node.js optimal headroom for storing connection context objects.

Step 4: Verification and Load Testing

Never assume your configurations are working without structured empirical testing. To simulate 100,000 concurrent WebSockets, you cannot rely on standard benchmarking tools from a single local machine, as client machines face the exact same ephemeral port limitations as servers.

Utilize distributed load testing frameworks like Artillery or Tsung deployed across multiple distinct cloud instances to generate realistic parallel traffic. During execution, run the following command on your target VPS to verify socket performance in real-time:

ss -s

This command outputs an accurate live breakdown of total transport sockets, ensuring your system handles the targeted load effortlessly while remaining highly responsive.

Conclusion

Reaching 100,000 concurrent WebSockets on a single Linux VPS is entirely achievable when the application tier and kernel operate in harmony. By expanding system file handles, shrinking TCP memory buffers, and scaling across CPU cores with a process manager, you transform a standard server into an enterprise-grade real-time engine. Implementing these system-level adjustments safeguards infrastructure longevity, improves end-user latency, and decreases overall computing costs.

Deep Linux Kernel Tuning: Scaling Node.js VPS to 100,000 Concurrent WebSockets | DPTCloud