WritingWebSocket Load Balancing: Sticky Sessions, Proxy Config, and Common Failures — Clixo
6 min readwebsockets, load-balancing, nginx, infrastructure, sticky-sessions

WebSocket Load Balancing: Sticky Sessions, Proxy Config, and Common Failures

A practical guide to WebSocket load balancing—how sticky sessions work, NGINX and AWS ALB configuration, and why standard HTTP load balancing breaks WebSocket apps.

You scaled your WebSocket server to multiple instances, put a load balancer in front, and immediately started seeing errors. Messages get lost. Some clients never receive events. Reconnections cause strange state issues. This is not a bug in your application — it is a fundamental incompatibility between how standard HTTP load balancing works and how WebSocket connections behave. Here is what you need to know.

Why Standard HTTP Load Balancing Breaks WebSockets

HTTP load balancers distribute requests across backend servers using round-robin or least-connection algorithms. Each request is independent — it can go to any available server, and the server does not need to remember anything about previous requests from the same client.

WebSockets are stateful. When a client connects, the server builds an in-memory record of that connection: its socket reference, the user's authenticated identity, which rooms it has joined, and any session-specific state. All subsequent messages from that client must reach the same server — the one holding the socket object and the connection state.

If the load balancer sends a WebSocket frame from client A to server 2, but client A's connection is on server 1, server 2 has no idea what to do with it. The frame is dropped or rejected.

Sticky Sessions: The Core Fix

Sticky sessions (also called session affinity) configure the load balancer to route all traffic from a specific client to the same backend server for the lifetime of the connection.

There are two common methods:

IP Hash — the load balancer hashes the client's IP address and consistently routes that IP to the same backend. Simple to configure, but fails when many users share a single IP (corporate NAT, proxies, CGNAT). All users behind that IP land on the same server, defeating the load-balancing purpose.

Cookie-based affinity — the load balancer sets a session cookie on the first request. Subsequent requests from that client carry the cookie, and the load balancer uses it to route to the correct backend. More reliable than IP hash in most production environments.

NGINX Configuration for WebSocket Load Balancing

A minimal NGINX upstream configuration for WebSocket sticky sessions using IP hash:

upstream ws_backend {
  ip_hash;
  server app1.internal:3000;
  server app2.internal:3000;
  server app3.internal:3000;
}
 
server {
  listen 443 ssl;
  server_name api.example.com;
 
  location /ws {
    proxy_pass http://ws_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
  }
}

The critical headers are Upgrade and Connection "upgrade" — without these, NGINX will not pass the WebSocket upgrade request to the backend. proxy_read_timeout must be set high enough to accommodate long-lived idle connections; the default NGINX timeout of 60 seconds will close connections that have not received data recently.

AWS ALB Configuration

On AWS Application Load Balancer, enable sticky sessions on the target group:

  1. Navigate to your target group in the EC2 console
  2. Edit target group attributes
  3. Enable "Stickiness" and set the stickiness type to "Load balancer generated cookie"
  4. Set the duration long enough to cover a typical session (several hours to days)

ALB also requires the listener to pass the WebSocket upgrade. ALB handles WebSocket natively as long as the target group protocol is HTTP (not HTTPS — TLS termination happens at the ALB).

Note that ALB has an idle connection timeout of 60 seconds by default. Increase this (up to 4000 seconds) or ensure your WebSocket heartbeat interval is shorter than the timeout. A heartbeat every 30 seconds with a 120-second ALB timeout is a reasonable combination.

Kubernetes Ingress for WebSocket Traffic

If you are running on Kubernetes, your ingress controller needs explicit configuration for WebSocket proxying and sticky sessions.

For NGINX Ingress Controller:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: websocket-ingress
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "ws-affinity"
    nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"
    nginx.ingress.kubernetes.io/upstream-hash-by: "$remote_addr"
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /ws
            pathType: Prefix
            backend:
              service:
                name: websocket-service
                port:
                  number: 3000

The affinity: cookie annotation enables cookie-based sticky sessions at the ingress layer.

The Limits of Sticky Sessions

Sticky sessions solve the routing problem but do not solve the state synchronization problem. When a pod restarts or is terminated (during a deploy, a crash, or a scale-down event), all connections on that pod drop. Clients reconnect, and sticky sessions route them to whichever pod is available — which may not have any knowledge of their previous session state.

This is why sticky sessions must be combined with a shared state layer. Presence, room membership, and message history need to live in a shared store (Redis, a database) rather than purely in-process memory. After reconnection, clients should re-authenticate and re-subscribe, and the server should be able to restore their state from the shared store regardless of which instance they land on.

A Common Failure Mode: The Proxy That Drops Upgrades

Some corporate proxies, reverse proxies misconfigured without the Upgrade header passthrough, and certain CDN configurations silently drop WebSocket upgrade requests. The client receives a 200 OK for the initial HTTP request but the connection never upgrades, or it upgrades and then drops silently after 30 seconds.

Diagnose this by checking the connection lifecycle in your browser's Network tab:

  • Look for the initial HTTP request to your WebSocket endpoint
  • Check its status code (should be 101 Switching Protocols)
  • If you see 200 or 400, the upgrade failed at a proxy layer
  • Check the response headers for the presence of Upgrade: websocket and Connection: upgrade

If you cannot fix the proxy, falling back to SSE or long polling for affected clients is the pragmatic path.


Getting WebSocket load balancing right is a prerequisite for any production real-time system. If you are designing real-time infrastructure and want to avoid the failure modes before they hit production, Clixo can help you architect and ship it.