ThisCalculator

Everyday

Round Robin WebSocket Scaling: Key Facts

Updated · Standard formula, shown below

Round Robin WebSocket Scaling: Key Facts

Round-robin load balancing fails for WebSockets at scale because it distributes connections without considering the server’s current load or the persistent, stateful nature of WebSocket sessions, leading to uneven resource use, connection limits, and broken features like broadcasting or sticky sessions. This article explains why this happens and what to use instead.

Background: What WebSockets Need That HTTP Doesn’t

HTTP requests are short-lived and stateless: a client asks, a server answers, and the connection closes. A load balancer can send each request to any available server because the servers share no session state (or can easily share it via a database or cache). WebSockets, however, open a single TCP connection that stays alive for minutes or hours. The server holds that connection open, tracks its state, and may need to push messages to it at any time. That changes everything about load balancing.

Key differences that matter for load balancing:

  • Connection persistence: A WebSocket connection is not closed after a response; it stays open, consuming memory, file descriptors, and CPU on the server that accepted it.
  • Statefulness: The server often keeps per-client state (e.g., user ID, room, authentication token) in memory tied to that connection.
  • Server-to-client push: The server may need to send messages to a specific client, so it must know which server holds that client’s connection.
  • Connection limits: Each server has a finite number of file descriptors and memory; a single server can only hold so many concurrent connections.

How Round Robin Works (and Its Core Assumption)

Round-robin load balancing is simple: the load balancer maintains a list of backend servers and forwards each new request to the next server in the list, cycling through them in order. It assumes all requests are roughly equal in cost and that the connection is short-lived. For HTTP, that’s often true. For WebSockets, it’s not.

The load balancer only sees the initial HTTP upgrade request (the handshake). It forwards that request to the next server in the round-robin list, and the WebSocket connection is then pinned to that server for its entire lifetime. The load balancer does not track how many active WebSocket connections each server currently holds, nor how much CPU or memory each is using. It just cycles blindly.

Why Round Robin Breaks at Scale

At small scale (a few hundred connections), round robin works fine because the imbalance is small. At scale (thousands or millions of connections), the flaws become critical:

  • Uneven load: Some clients connect for seconds, others for hours. Round robin gives each new connection to the next server in line, so a server that received several long-lived connections early will still be holding them hours later, while a server that got many short-lived connections is free. The load balancer never rebalances.
  • Connection exhaustion: Each server has a hard limit on concurrent connections (often set by the OS or application, e.g., 65,535 per IP, or lower due to memory). If round robin sends too many connections to one server, that server hits its limit and refuses new connections, even though other servers have plenty of capacity.
  • Broken broadcasting: In a chat app or live dashboard, the server needs to push messages to all clients in a room. If clients are spread across many servers, the server that receives the message must forward it to other servers, adding complexity and latency. Round robin makes this worse because it scatters connections randomly.
  • Sticky session failures: Some WebSocket features require a client to always connect to the same server (e.g., if the server stores session data in memory). Round robin does not provide stickiness; a client that reconnects (after a network blip) may land on a different server and lose its state.

Concrete Example: A Chat Application

Imagine a chat service with 10 backend servers and 10,000 active WebSocket connections. With round robin, the 10,000 connections are distributed roughly evenly at the start—about 1,000 per server. But users connect and disconnect at different times. Over an hour, suppose 2,000 users on server A stay connected, while 1,500 users on server B disconnect and are replaced by new ones. Now server A is holding 2,000 connections, server B only 500. The imbalance grows. If the service grows to 100,000 connections, some servers may exceed their connection limit (say 20,000) and start dropping new connections, while others sit at 5,000. The service becomes unreliable even though total capacity is sufficient.

Broadcasting a message to all 10,000 users now requires each server to send to its own subset, but the server that receives the broadcast must know which server holds which user. Without a shared pub/sub layer (like Redis), it cannot. Round robin doesn’t solve this; it makes it harder because users are scattered arbitrarily.

What to Use Instead: Better Load Balancing Strategies

For WebSockets at scale, you need a load balancer that understands connection state and server capacity. Options include:

  • Least connections: The load balancer tracks the number of active connections on each server and sends a new connection to the server with the fewest. This avoids the blind cycling problem and keeps connection counts balanced.
  • IP hash: The load balancer hashes the client’s IP address to choose a server. This ensures the same client always connects to the same server (sticky), but it can cause uneven distribution if many clients share an IP (e.g., behind a NAT).
  • Sticky sessions with cookie: The load balancer sets a cookie during the handshake and then routes subsequent connections from the same client to the same server. This is useful for stateful WebSocket apps, but it can cause uneven load if some users are much more active.
  • Layer 4 vs Layer 7: A Layer 4 load balancer (e.g., HAProxy in TCP mode) forwards raw TCP traffic and can do least-connections. A Layer 7 load balancer (e.g., Nginx with WebSocket proxy) understands HTTP upgrade and can also do sticky routing. Both are better than round robin for WebSockets.

Common Misconceptions and Pitfalls

One common misconception is that round robin is fine because WebSocket connections are cheap. They are not: each connection consumes a file descriptor, kernel memory, and often application memory for buffers and state. A server that holds 10,000 connections is using significant resources, even if idle.

Another pitfall is assuming that the load balancer will rebalance existing connections. It won’t. Round robin only decides where a new connection goes; it never moves an existing connection. So even if you switch to least-connections, you still have to wait for old connections to close before the balance evens out. In practice, you may need to implement a rebalancing mechanism (e.g., gracefully close connections on an overloaded server and let clients reconnect).

Also, beware of health checks. Round robin load balancers typically check if a server is up (TCP or HTTP ping). But a server can be up yet overloaded; round robin will still send it new connections. Least-connections with a health check that also monitors connection count is better.

Frequently Asked Questions

Does round robin work for WebSockets at all?

Yes, for small deployments (a few hundred connections) or where connections are short-lived (e.g., a quick notification stream), round robin can work fine. The problems appear when you have thousands of long-lived connections and need to guarantee even distribution and low latency.

What is the maximum number of WebSocket connections a server can handle?

It varies widely. A typical Linux server can handle tens of thousands of connections if configured properly (raising file descriptor limits, using epoll). But practical limits are often set by memory: each connection may use 10-50 KB of application memory, so 10,000 connections could use 100-500 MB just for state. The exact number depends on your hardware and application.

Can I use round robin with sticky sessions?

Round robin itself does not provide stickiness. You can combine round robin with a cookie-based sticky session (the load balancer remembers which server it chose for a client), but that still doesn’t balance by load—it just balances by connection count, and only for new connections.

Why does my WebSocket connection drop when I scale out?

If you add more servers and use round robin, existing connections stay on their original server. When that server restarts or hits its connection limit, those connections drop. Clients may reconnect to a different server, but if your app requires state on the original server, it breaks. Use sticky sessions and a shared state store (like Redis) to avoid this.

Is there a way to make round robin work for WebSockets?

You can mitigate some issues by using round robin with a very short connection lifetime (e.g., force reconnects every few minutes), but that’s rarely acceptable for real-time apps. A better approach is to use least-connections or a dedicated WebSocket load balancer that supports connection draining and rebalancing.

What about using a message broker like Redis or Kafka?

A message broker (pub/sub) decouples the client connection from the server that processes messages. When a server receives a message, it publishes it to a channel; all servers subscribed to that channel receive it and forward to their own clients. This makes load balancing less critical because any server can handle any message. But you still need to balance the WebSocket connections themselves, so round robin alone is still not ideal.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.