Chat apps, live dashboards, multiplayer games and tools such as Socket.IO keep a WebSocket open between the browser and your server. A plain Nginx reverse proxy breaks these connections, because the WebSocket handshake relies on HTTP headers that Nginx does not forward by default. This guide adds the missing headers, prevents idle sockets from being cut after a minute, and shows how to test the connection.
Before you start
- A Linux VPS with Nginx in front of your app. If you have not set up a proxy yet, start with Set up Nginx as a reverse proxy.
- Your app accepting WebSocket connections on a local port. This guide uses
127.0.0.1:3000. - HTTPS on the site, so browsers can use secure
wss://sockets. - SSH access with sudo.
Why WebSockets need extra config
A WebSocket starts as a normal HTTP request with two special headers:
Upgrade: websocket
Connection: UpgradeThe server answers 101 Switching Protocols and the connection becomes a two-way socket. Two things stop this from working through Nginx by default:
UpgradeandConnectionare hop-by-hop headers. Nginx does not pass them to the backend unless you set them explicitly.- Nginx speaks HTTP/1.0 to backends by default, and the upgrade requires HTTP/1.1.
Add the connection_upgrade map
The Connection header should be upgrade for WebSocket requests and close (or empty, for keepalive) for everything else. A map sets this automatically. It must live in the http context, so put it in its own file:
sudo nano /etc/nginx/conf.d/websocket-map.confmap $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}When the browser sends an Upgrade header, $connection_upgrade becomes upgrade. For ordinary requests it becomes close.
Note: Some installs already define this map, for example a config added by a control panel. Search first with sudo grep -rn connection_upgrade /etc/nginx/. Defining the same variable twice makes nginx -t fail.
Proxy the WebSocket path
In your site's server block, add a location for the socket endpoint. Socket.IO uses /socket.io/; many other apps use /ws:
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 $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;
}If your whole app uses WebSockets on any path, you can put the Upgrade and Connection lines in your main location / instead. Ordinary requests are unaffected because the map sends close for them.
Handle idle timeouts
proxy_read_timeout defaults to 60 seconds. If no data passes in either direction for that long, Nginx closes the socket. Quiet chat rooms and dashboards often hit this.
You have two options, and using both is best:
- Raise
proxy_read_timeoutfor the socket location, as above. - Have the app send a ping every 20 to 30 seconds. Socket.IO and most WebSocket libraries do this by default.
Do not raise the timeout on your main location / just for sockets. A long read timeout there lets hung requests tie up connections.
Many open sockets
Each WebSocket holds a connection open, and Nginx counts both the client side and the backend side. If you expect thousands of concurrent users, check worker_connections in /etc/nginx/nginx.conf and the open file limit. See Tune Nginx worker processes and connections.
Apply the change
sudo nginx -t && sudo systemctl reload nginxA reload keeps existing WebSocket connections alive on the old worker processes until they close, so users are not kicked off.
Check it works
In the browser, open developer tools, go to the Network tab, filter by WS, and reload your app. The socket request should show status 101. Click it to watch messages in the Messages tab.
From the command line, check the handshake with curl. The key value can be any base64 string of 16 bytes:
curl -i -N --http1.1 \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
https://app.example.com/wsA working proxy returns:
HTTP/1.1 101 Switching Protocols
upgrade: websocket
connection: upgradePress Ctrl + C to close it. For Socket.IO, the path needs query parameters, so the browser test is easier.
Troubleshooting
- Handshake returns 400 or 200 instead of 101: the
UpgradeandConnectionheaders are not forwarded, orproxy_http_version 1.1is missing. Check the location that actually matches the socket path. - Socket connects then drops after about 60 seconds:
proxy_read_timeoutis too short and the app sends no pings. Raise it for that location. nginx -tsays "duplicate map variable":$connection_upgradeis defined twice. Keep one map.- Works on HTTP but not HTTPS: the page uses
ws://on an HTTPS site and the browser blocks it as mixed content. Usewss://. - Behind Cloudflare, sockets disconnect: make sure WebSockets are enabled in the Cloudflare dashboard and the app sends regular pings.
Next steps
- Run a Node.js app in production: Deploy a Node.js app with PM2 and Nginx.
- Several app instances behind one domain: Load balance backends with Nginx upstream.
- Real-time apps need steady resources.
