Back to articles
Technology Insight

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

May 29, 2026

Introduction: The Challenge of Scale

In modern real-time architecture, sustaining high-density concurrent connections is a definitive benchmark of engineering efficiency. While Node.js is naturally suited for I/O-bound tasks due to its asynchronous, event-driven paradigm, deploying a WebSocket server capable of handling 100,000 concurrent connections involves constraints that transcend the application layer. By default, standard Linux distributions are configured as general-purpose operating systems, imposing conservative limits on file descriptors, memory allocation, and network stack capacity to prevent resource starvation.

To scale a Node.js Virtual Private Server (VPS) to the 100k milestone, system administrators and DevOps engineers must engage in Deep Kernel Tuning. This technical guide explores the exact subsystems, network parameters, and resource limitations that must be optimized within the Linux kernel to transform a standard VPS into a high-performance, real-time communications engine.

1. Redefining System-Wide Resource Limits

Every WebSocket connection represents an open network socket, which the Linux operating system treats fundamentally as a file. Consequently, the first and most immediate barrier to scaling connections is the File Descriptor (FD) limit. If your system runs out of FDs, Node.js will fail to accept new connections, emitting fatal EMFILE errors.

Modifying Hard and Soft Limits

To safely accommodate 100,000 concurrent WebSockets—plus the necessary system overhead—the operating system limitations must be drastically elevated. This is achieved by modifying the /etc/security/limits.conf file to configure the limits for the specific user running the Node.js process (e.g., nodeuser):

nodeuser soft nofile 150000
nodeuser hard nofile 150000

The soft limit represents the value that the kernel enforces but allows the process to increase up to the hard limit, which acts as an absolute ceiling. Setting both to 150,000 ensures an adequate buffer for system processes, log files, and database connections alongside the primary WebSocket traffic.

System-Wide Configuration

Beyond individual user profiles, the kernel enforces a global ceiling on open files across all active processes. To inspect and adjust this value globally, the fs.file-max parameter must be amended via sysctl:

  • View current limit: sysctl fs.file-max
  • Apply persistent scaling: Add fs.file-max = 200000 to /etc/sysctl.conf

2. Advanced Networking Stack Optimization via sysctl

Once file descriptor limitations are resolved, the primary focus shifts to the Linux network subsystem (netdev and ipv4). The default kernel queuing and handshake processing mechanisms cannot keep pace with high-density, concurrent workloads without modification.

Optimizing Connection Queues

When thousands of clients attempt to connect simultaneously, they enter a backlog queue before the Node.js application can formally accept them. If this queue saturates, packets are silently dropped, triggering connection timeouts on the client side. We optimize three distinct queues to alleviate this risk:

  1. net.core.somaxconn: Controls the maximum backlog of established connections waiting to be accepted by the application. Raise this value from the typical default of 128 to 65535.
  2. net.ipv4.tcp_max_syn_backlog: Defines the maximum number of half-open connections (clients that have sent a SYN packet but have not completed the 3-way handshake). Increase this to 65535 to buffer spikes in connection requests.
  3. net.core.netdev_max_backlog: Specifies the maximum number of packets allowed to queue on the network interface card (NIC) input side before being processed by the CPU. Scale this to 65535.

Managing ephemeral Ports and Local Subsystems

If your VPS communicates with internal load balancers, reverse proxies (like Nginx), or microservices via upstream TCP connections, it can quickly exhaust local ephemeral ports. By default, Linux reserves a narrow range. We widen this allocation to maximize throughput:

net.ipv4.ip_local_port_range = 1024 65535

Additionally, enabling TCP Time-Wait Reuse allows the kernel to safely recycle sockets in the TIME_WAIT state for new connections, which dramatically reduces socket accumulation under heavy turnover: net.ipv4.tcp_tw_reuse = 1.

3. Memory Allocation and TCP Buffer Management

Every open TCP connection requires dedicated read and write memory buffers. While it is tempting to allocate large buffers to maximize data throughput per packet, doing so indiscriminately will induce an Out Of Memory (OOM) killer event when scaling to 100,000 connections.

WebSockets are typically characterized by frequent, small, real-time message payloads rather than massive data streams. Therefore, we should configure the kernel to use smaller initial memory footprints that scale dynamically based on real-time demands.

Fine-Tuning TCP Read and Write Vectors

The parameters net.ipv4.tcp_rmem (read) and net.ipv4.tcp_wmem (write) accept three values defining the minimum, default, and maximum byte allocations per socket. To safely achieve our target on a mid-tier VPS, apply the following tuning:

net.ipv4.tcp_rmem = 4096 8192 16777216
net.ipv4.tcp_wmem = 4096 8192 16777216

By dropping the default allocation down to 8KB per connection, 100,000 idle or low-throughput WebSockets will theoretically occupy roughly 1.6GB of RAM for networking buffers alone. This leaves plenty of system memory headroom for the Node.js V8 runtime engine to execute business logic, manage the heap, and perform garbage collection without crashing the host OS.

4. Tailoring Node.js for High-Density WebSocket Throughput

Kernel tuning represents half of the battle; the Node.js application layer must be explicitly configured to interface efficiently with these upgraded OS capabilities.

Selecting the Right WebSocket Library

The standard, native mechanisms in Node.js can carry significant abstraction overhead. For 100k concurrency, utilizing highly optimized C++ bindings is critical. Developers should favor libraries like ws or uWebSockets.js over heavier abstractions like Socket.io unless specific fallback features are absolutely mandatory. The uWebSockets.js library, in particular, is written in native C++ and demonstrates significantly lower memory overhead per connection compared to pure JavaScript alternatives.

Leveraging Multiple CPU Cores via Clustering

Because Node.js operates on a single-threaded event loop, it cannot naturally exploit multi-core VPS environments. To distribute the processing overhead of encryption (TLS/SSL handshakes) and incoming data parsing across all available CPU threads, implement the native Cluster module or utilize a process manager like PM2:

pm2 start server.js -i max

When running clustered instances behind a single port, ensure that the kernel's reuse-port feature is supported, enabling multiple worker threads to bind to the same port efficiently and balance connection distribution at the hardware interrupt level.

Conclusion: A Production-Ready Architecture

Scaling a Node.js VPS to support 100,000 concurrent WebSockets requires moving beyond the limits of application code into the underlying architecture of the operating system. By scaling file descriptor boundaries, optimizing packet backlogs, dynamically balancing TCP memory footprints, and pairing these modifications with multi-core Node.js execution, you create a robust ecosystem capable of extreme real-time scale.

Before launching to production, always validate your specific kernel modifications using distributed load-testing tools like Artillery or k6 to simulate realistic connection ramp-ups, monitoring memory pressure closely to guarantee sustained infrastructure stability.

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