WebSockets start as a normal HTTP request with an Upgrade: websocket header; the server answers 101 Switching Protocols and the connection becomes a long-lived, two-way channel. Nginx proxies WebSockets fine — but only if you pass the upgrade headers explicitly and allow long-lived connections.

Minimal configuration

location /ws/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 1h;
}
  • proxy_http_version 1.1 — the upgrade mechanism requires HTTP/1.1 (Nginx defaults to 1.0 upstream).
  • Upgrade and Connection are hop-by-hop headers that Nginx doesn’t forward by default; you must set them.
  • proxy_read_timeout — Nginx closes a proxied connection after 60 seconds without data from the backend. Long-lived sockets need a longer timeout, or application-level pings.

The map pattern: one location for HTTP and WebSockets

If the same path serves normal HTTP requests and WebSockets (common for apps like Uptime Kuma, Home Assistant, Grafana Live, Socket.IO), use a map so Connection: upgrade is only sent when the client asked for an upgrade:

# http {} context
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 1h;
    proxy_send_timeout 1h;
}

With upstream keepalive enabled, use '' → "" (empty) instead of close so normal requests can reuse connections.

Socket.IO

Socket.IO uses the path /socket.io/ and starts with HTTP long-polling before upgrading:

location /socket.io/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
}

Python ASGI (FastAPI, Django Channels)

Uvicorn and Daphne handle WebSockets natively; the same Nginx config applies. Make sure the app trusts proxy headers (--proxy-headers). See FastAPI with Uvicorn.

Load balancing WebSocket servers

A WebSocket stays on the backend it connected to. With several backends:

upstream ws_backend {
    least_conn;
    server 10.0.0.11:3000;
    server 10.0.0.12:3000;
}

least_conn spreads long-lived connections evenly. If your app needs a client to always reach the same server (e.g. Socket.IO with HTTP polling fallback), add stickiness: ip_hash; or hash $cookie_io consistent;. Shared state between servers (Socket.IO’s Redis adapter) avoids the need for stickiness. See Nginx load balancing.

Connection limits

Each WebSocket holds a connection open. For thousands of concurrent clients, raise limits:

# nginx.conf
worker_rlimit_nofile 65535;
events {
    worker_connections 16384;
}

Each proxied WebSocket uses two connections in Nginx (client side and upstream side). Also raise the OS file descriptor limit for Nginx (LimitNOFILE in the systemd unit) — see ulimit and open files.

HTTPS and Cloudflare

wss:// (WebSocket over TLS) works automatically when the server block uses SSL. Cloudflare’s proxy supports WebSockets on all plans; idle connections are closed after about 100 seconds without traffic, so send periodic pings.

Debugging

  • Browser console: “WebSocket connection to ‘wss://…’ failed” — check the Network tab (WS filter) for the status code.
  • 400 Bad Request or 426 Upgrade Required — upgrade headers not passed, or proxy_http_version missing.
  • Disconnects every 60 seconds — proxy_read_timeout too low; or no ping/pong.
  • Works locally, fails in production — another proxy (load balancer, CDN, Nginx Proxy Manager) in the chain lacks WebSocket support or the headers.

Test the handshake with curl:

curl -i -N --http1.1 -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
  https://example.com/ws/

HTTP/1.1 101 Switching Protocols means the proxy and backend are working.

Related: WebSockets explained, secure WebSocket servers and the Nginx reverse proxy guide.

  • Nginx
  • WebSockets
  • Reverse Proxy