Linux VPS

Proxy WebSockets through Nginx: Upgrade headers, timeouts and scaling

Configure Nginx to proxy WebSocket connections for chat, live dashboards and Socket.IO apps: the Upgrade and Connection headers, long read timeouts, sticky sessions and debugging failed handshakes.

IntermediateLinux VPS3 min read

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

  1. The browser sends GET /ws with Upgrade: websocket and Connection: Upgrade.
  2. The server answers 101 Switching Protocols.
  3. 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 map sends Connection: upgrade for WebSocket requests and close for 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_hash or hash $remote_addr consistent in 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

  1. In the browser DevTools → Network → WS, the request should show status 101.
  2. From the shell with a WebSocket client such as websocat:
    websocat wss://example.com/ws/
  3. 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_timeout at 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://. Use wss://.

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.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.