Websites & apps

Proxy WebSocket connections through Nginx

Make WebSockets work behind Nginx by passing the Upgrade and Connection headers, using a map for connection_upgrade, raising read timeouts, and testing the socket.

IntermediateLinux server5 min read

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: Upgrade

The server answers 101 Switching Protocols and the connection becomes a two-way socket. Two things stop this from working through Nginx by default:

  • Upgrade and Connection are 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:

Bash
sudo nano /etc/nginx/conf.d/websocket-map.conf
Nginx
map $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:

Nginx
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_timeout for 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

Bash
sudo nginx -t && sudo systemctl reload nginx

A 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:

Bash
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/ws

A working proxy returns:

HTTP/1.1 101 Switching Protocols
upgrade: websocket
connection: upgrade

Press 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 Upgrade and Connection headers are not forwarded, or proxy_http_version 1.1 is missing. Check the location that actually matches the socket path.
  • Socket connects then drops after about 60 seconds: proxy_read_timeout is too short and the app sends no pings. Raise it for that location.
  • nginx -t says "duplicate map variable": $connection_upgrade is 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. Use wss://.
  • Behind Cloudflare, sockets disconnect: make sure WebSockets are enabled in the Cloudflare dashboard and the app sends regular pings.

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.