WebSockets keep a connection open between browser and server so either side can send messages at any time: chat, notifications, live dashboards, multiplayer games. A WebSocket starts as a normal HTTP request asking to upgrade, and Nginx only passes that through if you tell it to. This guide covers a correct Nginx WebSocket proxy configuration.
Before you start
- An app serving WebSockets locally, e.g. Node.js on
127.0.0.1:3000. - Nginx as a reverse proxy with HTTPS: Nginx reverse proxy setup. Browsers on HTTPS pages must use
wss://, which Nginx terminates like normal HTTPS.
How the upgrade handshake works
- The browser sends
GET /wswithUpgrade: websocketandConnection: Upgrade. - The server answers
101 Switching Protocols. - From then on the TCP connection carries WebSocket frames, not HTTP.
Upgrade and Connection are hop-by-hop headers: proxies do not forward them by default. That's why a plain proxy_pass fails for WebSockets.
The configuration
In the http context:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}In the server block:
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 $connection_upgrade;
proxy_set_header Host $host;
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;
}proxy_http_version 1.1: the upgrade mechanism needs HTTP/1.1 to the backend.- The
mapsendsConnection: upgradefor WebSocket requests andclosefor normal ones, so you can use one location for both. proxy_read_timeout: Nginx closes a proxied connection if nothing is received for this long (default 60 seconds). Raise it, and/or make the app send ping frames every 20–30 seconds.
For Socket.IO, the path is usually /socket.io/; for apps that use WebSockets on every path, put the directives in location /.
Scaling across several backends
A WebSocket stays connected to the backend that accepted it. With several app servers:
- Socket.IO with HTTP long-polling fallback needs sticky sessions, because the polling requests must hit the same server. Use
ip_hashorhash $remote_addr consistentin the upstream, or configure clients to use the WebSocket transport only. - Broadcasts across servers need a shared message bus (e.g. the Redis adapter for Socket.IO).
Upstream setup: Nginx load balancing.
Connection limits
Each WebSocket holds one client connection and one backend connection open. For thousands of users, raise worker_connections and the open-file limit (too many open files). A reload keeps old workers alive until their WebSockets close, so you may briefly see more Nginx processes than usual; worker_shutdown_timeout caps that.
Test the connection
- In the browser DevTools → Network → WS, the request should show status
101. - From the shell with a WebSocket client such as
websocat:websocat wss://example.com/ws/ - Or check the handshake with curl:
curl -si -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' https://example.com/ws/ | head -1
Troubleshooting
- Handshake returns 400 or 200 instead of 101: Upgrade/Connection headers not forwarded, or the request hit a different location.
- Connections drop after exactly 60 seconds:
proxy_read_timeoutat default and no pings. - Works locally, fails behind Cloudflare: enable WebSockets in the CDN settings; check its idle timeout.
- Mixed content error: the page is HTTPS but the script connects to
ws://. Usewss://.
FAQ: Nginx WebSocket proxy
Does HTTP/2 change anything?
Browsers usually open WebSockets over HTTP/1.1 connections; Nginx handles this transparently.
Can Apache or LiteSpeed proxy WebSockets?
Yes. See Apache reverse proxy and Node.js behind Nginx, Apache or LiteSpeed.
How many WebSockets can one server handle?
Tens of thousands with enough RAM and raised limits; the app is usually the bottleneck.
