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).UpgradeandConnectionare 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 Requestor426 Upgrade Required— upgrade headers not passed, orproxy_http_versionmissing.- Disconnects every 60 seconds —
proxy_read_timeouttoo 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.