WebSockets keep a connection open between browser and server for live chat, dashboards and notifications. They start as a normal HTTP request with an Upgrade header, and any proxy in between must pass that header on. This guide shows how to do that for each web server and how to avoid dropped connections.
Before you start
- A Node.js app using
wsor Socket.IO that works locally. - A reverse proxy in front of the app: Node.js reverse proxy.
Nginx
Add the upgrade headers and longer timeouts to the location that serves your app:
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;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}Reload with sudo nginx -t && sudo systemctl reload nginx. A deeper Nginx walkthrough is in Nginx WebSocket proxy.
Apache
- Enable the modules:
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite - In the virtual host, before the normal
ProxyPass:RewriteEngine On RewriteCond %{HTTP:Upgrade} websocket [NC] RewriteCond %{HTTP:Connection} upgrade [NC] RewriteRule ^/?(.*) ws://127.0.0.1:3000/$1 [P,L] ProxyTimeout 3600 - Reload Apache.
LiteSpeed and OpenLiteSpeed
A proxy context pointing to a Web Server external app passes WebSocket upgrades automatically. If connections drop after a minute, raise the external app's Connection Keepalive Timeout and Initial Request Timeout.
On cPanel hosting
Apps from Setup Node.js App run through Passenger. Basic WebSocket traffic generally works, but long-lived connections are limited by shared hosting process and connection limits. For best results:
- Use Socket.IO, which falls back to HTTP long-polling if a WebSocket upgrade is blocked.
- Keep the app to a single process so connections share state.
- For many concurrent users, move the real-time part to a VPS.
Use wss:// with SSL
A page served over HTTPS must connect with wss://. Terminate SSL at the proxy (SSL for Node.js apps); the proxy then talks plain ws:// to Node.js. In the browser, connect relative to the page so the scheme matches:
const socket = io(); // Socket.IO picks wss:// on HTTPS pagesKeep connections alive
- Send a ping every 25–30 seconds. Socket.IO does this by default; with
wsimplement ping/pong. - Make the proxy timeout longer than the ping interval.
- Cloudflare and other CDNs also close idle sockets after about 100 seconds.
Scaling to several processes
With PM2 cluster mode or several servers, a client may reconnect to a different process. Use sticky sessions (ip_hash in an Nginx upstream) and the Socket.IO Redis adapter so all processes share events.
Troubleshooting
- Handshake fails with 400 —
Upgrade/Connectionheaders are not forwarded. - Connects then drops after 60 seconds — proxy read timeout is too short.
- Mixed content error — the page is HTTPS but the code connects to
ws://. - Works with one instance, fails with two — add sticky sessions and a shared adapter.
