Skip to content

Building Scalable WebSocket Servers in Node.js

CoreConceptAugust 3, 20269 min read

Building real-time applications — such as crypto market data feeds, multiplayer gaming servers, or live chat applications — requires maintaining hundreds of thousands of long-lived open TCP WebSocket connections. Standard JavaScript WebSocket libraries (like ws or Socket.IO) running on standard Node.js event loops suffer from high V8 heap memory overhead (~30KB to 50KB per socket) and Garbage Collection (GC) latency spikes.

uWebSockets.js is an ultra-high performance C++ native WebSocket server bound directly to Node.js. By handling socket I/O, framing, and memory allocations in C++ via libusockets, uWebSockets.js handles 100,000+ concurrent connections per server instance while consuming up to 8.5x less RAM. This guide details uWebSockets.js architecture, native topic Pub/Sub, backpressure management, and connection heartbeats.

uWebSockets.js high-concurrency architecture components and C++ fast-path messaging
uWebSockets.js high-concurrency architecture components and C++ fast-path messaging

Mental Model: C++ Fast-Path Binding vs Standard Node.js ws Event Loop

To understand how uWebSockets.js achieves extreme throughput, contrast native C++ socket handling against standard JavaScript event loops.

Standard libraries (like ws) instantiate V8 JavaScript objects (EventEmitter, Buffer, Socket) for every incoming connection. Parsing WebSocket frame headers in JavaScript causes frequent V8 heap allocations, triggering Garbage Collection pauses under high message volumes.

uWebSockets.js offloads the entire networking layer to a lightweight C++ core library (uSockets). C++ manages TCP sockets, TLS encryption, and frame parsing outside V8's heap memory. V8 JavaScript is invoked only when application event handlers explicitly run, keeping RAM usage under ~4KB per active socket. For real-time protocol comparisons, read real time collaborative apps crdts websockets and graphql subscriptions vs websockets for realtime data.

uWebSockets.js message broadcast lifecycle from C++ topic engine to client TCP sockets
uWebSockets.js message broadcast lifecycle from C++ topic engine to client TCP sockets

Quick reference

  • uWebSockets.js offloads TCP networking and frame parsing to a native C++ core.
  • Consumes ~4KB RAM per active socket connection compared to ~35KB+ in standard ws.
  • Eliminates V8 Garbage Collection pause spikes during high-throughput message bursts.
  • Bypasses Node.js stream overhead using direct C++ event loop integration.
  • Serves 100,000+ active connections per single server core with ease.

Remember this

Use uWebSockets.js to bypass V8 heap allocations and serve 100,000+ concurrent WebSocket connections with C++ performance.

Managing 100,000+ Concurrent Connections with Low V8 Heap Memory

High-concurrency socket servers require configuring operating system kernel limits alongside Node.js process flags.

By default, Linux limits maximum open file descriptors per process (ulimit -n 1024). Increase nofile limits in /etc/security/limits.conf (hard nofile 1048576, soft nofile 1048576) to allow a single Node.js process to open 100,000+ TCP sockets.

In uWebSockets.js, configure maxPayloadLength (e.g., 64KB) and idleTimeout (e.g., 120 seconds). Restricting maximum frame payload sizes prevents memory exhaustion attacks from malicious clients attempting to upload gigabyte binary payloads.

Quick reference

  • Increase OS file descriptor limits (ulimit -n 1000000) to allow 100k+ open TCP sockets.
  • Tune Linux kernel TCP settings (sysctl net.ipv4.tcp_max_syn_backlog) for high connection rates.
  • Set maxPayloadLength on uWebSockets.js app configuration to reject oversized payloads.
  • Configure idleTimeout to disconnect inactive client sockets automatically.
  • Monitor V8 heap memory usage using process.memoryUsage().heapUsed in Grafana metrics.

Remember this

Tune Linux OS file descriptor limits and set uWebSockets.js maxPayloadLength limits to support high concurrency.

Topic Pub/Sub Broadcasting & Socket Room Grouping

Broadcasting a single message to 50,000 connected subscribers (e.g., streaming a stock price update) causes CPU bottlenecks if implemented via JavaScript loops (sockets.forEach(ws => ws.send(data))):

uWebSockets.js includes a native C++ Topic Pub/Sub engine. Clients subscribe to topics (ws.subscribe("crypto/btc-usd")). When a price update occurs, the server calls app.publish("crypto/btc-usd", message, uWS.OpCode.TEXT).

The C++ layer iterates subscribers and writes the message directly to TCP sockets in compiled C++ loops, bypassing V8 string encoding overhead entirely and delivering 10x faster broadcast speeds.

uWebSockets.js message broadcast lifecycle from C++ topic engine to client TCP sockets
uWebSockets.js message broadcast lifecycle from C++ topic engine to client TCP sockets

Quick reference

  • Native C++ Pub/Sub engine broadcasts messages to 50,000+ subscribers in microseconds.
  • ws.subscribe(topic) groups sockets into fast-path C++ topic subscription lists.
  • app.publish(topic, data) executes message serialization directly in compiled C++.
  • Eliminates JavaScript array iterations and duplicate V8 buffer serialization overhead.
  • Supports topic wildcard matching for multi-room chat and market channel feeds.

Remember this

Utilize native C++ app.publish() topic broadcasting to deliver 10x faster message fan-out speeds.

Handling Backpressure, Heartbeat Pings, & Graceful Disconnections

When a client browser experiences slow network connections, calling ws.send() continuously builds up unsent message bytes in server memory. If unmanaged, this backpressure leads to process memory crashes.

uWebSockets.js provides explicit backpressure inspection via ws.getBufferedAmount(). If ws.getBufferedAmount() > 64KB, pause sending data to that specific client or drop non-critical messages until the buffer clears (ws.drain event).

To detect dead client connections (such as mobile devices losing cell signal without sending a TCP FIN disconnect), configure uWebSockets.js idleTimeout. Native C++ ping/pong heartbeats automatically ping clients every 30 seconds, closing dead sockets promptly.

Quick reference

  • Check ws.getBufferedAmount() to detect slow client socket backpressure accumulation.
  • Listen to the ws.drain event to resume sending when client network buffer clears.
  • Native C++ ping/pong heartbeats detect silent client disconnections automatically.
  • idleTimeout closes dead sockets without requiring custom JavaScript ping timer interval loops.
  • Execute graceful server shutdown by closing listening sockets before draining active connections.

Remember this

Monitor ws.getBufferedAmount() backpressure and rely on native idleTimeout heartbeats to clear dead sockets.

Key takeaway

To test uWebSockets.js performance, launch a load test using autocannon -w 100 -c 10000 ws://localhost:9001. Confirm V8 heap memory remains stable under 50MB during high message bursts.

Share:

Related Articles

Building interactive, real-time web applications — such as live financial dashboards, collaborative document editors, or

Read

High-concurrency microservices demanding sub-millisecond API response times and tens of thousands of requests per second

Read

Rate Limiting is a critical defense mechanism for production APIs, protecting downstream microservices from traffic spik

Read

Keep learning

Follow a structured path or browse all courses to go deeper.