Production Checklist

Load balancer idle timeouts and sticky sessions, connection/file-descriptor limits, and graceful shutdown.

Load balancer configuration

A WebSocket connection is a long-lived TCP stream wearing an HTTP upgrade handshake as its entry ticket, and both halves of that need a load balancer's cooperation:

  • The upgrade handshake itself needs an HTTP-aware (L7) proxy to correctly recognize and forward Upgrade: websocket requests, rather than treating them like an ordinary short-lived request/response and cutting the connection the moment a normal HTTP request would be considered "done."
  • Idle timeouts are the far more common production surprise. A load balancer configured with, say, a 60-second idle timeout will silently close a WebSocket connection with no traffic for 60 seconds — even though both the client and the server think everything is fine. This is exactly why the heartbeat pattern from the previous page matters operationally, not just for detecting dead peers: regular ping/pong traffic keeps the connection from ever looking idle to anything in between.

Sticky sessions matter less than people initially assume: once a WebSocket handshake completes, every subsequent frame on that connection automatically stays on the same backend instance — there's no per-frame re-routing decision happening at all. What sticky sessions actually affect is what happens on the next connection: a reconnecting client (or a transport with an HTTP-polling fallback, like Socket.IO in its non-WebSocket mode) can land on a different backend instance without session affinity. A proper Redis-backed pub/sub backplane (covered on the scaling page in this track) makes that safe by design — any instance can serve any client, so it stops being a problem that stickiness needs to solve.

Connection limits

Every open WebSocket connection holds an open file descriptor on the server process, and most Linux systems ship with a default per-process limit (ulimit -n) far too low for thousands of concurrent connections — often 1024:

Bash
ulimit -n 65535

This has to be raised at the OS/process/container level for a WebSocket server expected to hold many concurrent connections — setting it inside application code alone isn't enough if the surrounding OS or container runtime still enforces a lower ceiling.

Beyond file descriptors, remember that an idle connection is not free — each one holds real server-side memory (buffers, whatever per-connection state the application attaches, like the ws.user object from the authentication page). Capacity planning for a WebSocket service means estimating peak concurrent connections per instance and provisioning memory and file descriptors for that number specifically, not just CPU for message throughput.

Graceful shutdown

Killing a server process outright during a deploy drops every connection it's holding all at once, forcing every one of those clients into a reconnect at (roughly) the same moment — precisely the thundering-herd scenario the reconnect backoff from the previous page exists to soften, but it's still better to avoid triggering it unnecessarily on every routine deploy.

A graceful shutdown handler intercepts SIGTERM, stops accepting new work, and gives existing connections a chance to close in an orderly way:

Javascript
let shuttingDown = false;

process.on('SIGTERM', () => {
  shuttingDown = true;
  console.log('Shutting down: closing connections gracefully...');

  for (const client of wss.clients) {
    client.send(JSON.stringify({ type: 'server_restart', reconnectAfterMs: 2000 }));
    client.close(1012, 'Server restarting'); // 1012: Service Restart
  }

  server.close(() => process.exit(0));

  // Force-exit if some clients don't close promptly
  setTimeout(() => process.exit(1), 10000);
});

wss.on('connection', (ws) => {
  if (shuttingDown) {
    ws.close(1012, 'Server restarting'); // don't accept new work mid-shutdown
  }
});

Sending an explicit server_restart message before closing lets a well-behaved client distinguish "the server told me it's restarting, reconnect deliberately" from an unexpected connection loss — useful for showing the user an accurate status instead of a generic error. Close code 1012 isn't part of the core WebSocket protocol's reserved range but is a widely recognized convention for exactly this situation, alongside the standard 1001 ("Going Away").

Monitoring what actually matters

The metrics that tell you a WebSocket service is healthy are different from the ones a typical stateless HTTP service leans on:

  • Concurrent open connections per instance — the core capacity number; a sudden drop usually means something is disconnecting clients unexpectedly.
  • Message throughput (in/out) — the WebSocket equivalent of request rate.
  • Ping/pong failure rate — how often the heartbeat is detecting and terminating dead connections; a spike often correlates with a network problem upstream of the server.
  • Reconnect rate — a sudden spike is one of the clearest signals of an outage, a bad deploy, or a load balancer misconfiguration (like the idle-timeout issue above), often visible in this metric before anything else shows it.

Production readiness checklist

Concern Naive setup Production-ready
Load balancer Default HTTP settings, short idle timeout Upgrade-aware, generous idle timeout backed by heartbeat traffic
Multi-instance messaging None — messages only reach clients on the same instance Redis (or similar) pub/sub backplane
Connection limits Default OS ulimit (often 1024) Raised limits, sized to expected peak concurrent connections
Shutdown Abrupt process kill SIGTERM handler: stop new connections, notify and close existing ones, force-exit timeout
Dead connections Never detected until a TCP-level timeout, if ever Ping/pong heartbeat with explicit termination of unresponsive sockets
Client reconnect Immediate retry loop Exponential backoff with jitter

Common mistakes

  • Configuring a load balancer with a short idle timeout and no heartbeat traffic underneath it, then chasing "random" disconnects that are actually the load balancer doing exactly what it was configured to do.
  • Leaving OS-level file-descriptor limits at their low interactive-shell defaults on a server meant to hold thousands of concurrent sockets.
  • Killing server processes on deploy with no graceful shutdown path, causing every connected client to reconnect at the same instant on every single release.
  • Monitoring only HTTP-style metrics (request rate, latency) with zero visibility into concurrent open connections or heartbeat failure rate — the metrics that actually indicate whether a WebSocket service is healthy.