Skip to content

Fix: Nginx WebSocket Proxy Not Working (101 Switching Protocols Failed)

FixDevs ·

Quick Answer

How to fix Nginx WebSocket proxying not working — 101 Switching Protocols fails, connections drop after 60 seconds, missing Upgrade headers, and SSL WebSocket configuration.

The Error

You configure nginx to proxy WebSocket connections but the browser console shows:

WebSocket connection to 'wss://example.com/socket' failed:
Error during WebSocket handshake: Unexpected response code: 502

Or:

WebSocket connection to 'ws://localhost/socket' failed:
Error during WebSocket handshake: net::ERR_CONNECTION_RESET

Or the WebSocket connects initially but drops after exactly 60 seconds:

WebSocket connection closed unexpectedly

Or the server returns 200 OK instead of 101 Switching Protocols.

Why This Happens

WebSocket connections start as HTTP requests and are upgraded to a persistent bidirectional connection using the 101 Switching Protocols response. Nginx does not proxy WebSocket connections by default — it treats them as regular HTTP and drops the Upgrade header.

Common causes:

  • Missing Upgrade and Connection headers in the nginx proxy config — required for WebSocket handshake.
  • proxy_http_version not set to 1.1 — WebSocket requires HTTP/1.1; nginx defaults to HTTP/1.0 for upstream connections.
  • Proxy timeout too short — nginx’s default proxy_read_timeout is 60 seconds, dropping idle WebSocket connections.
  • SSL termination misconfiguredwss:// (WebSocket over TLS) requires specific nginx SSL proxy setup.
  • Upstream not WebSocket-capable — the backend server needs to accept WebSocket connections on the proxied path.

Fix 1: Add the Required Upgrade Headers

The minimal nginx config to proxy WebSocket connections:

server {
    listen 80;
    server_name example.com;

    location /socket/ {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;

        # Required for WebSocket handshake
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Standard proxy headers
        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;
    }
}

The three critical settings:

  1. proxy_http_version 1.1 — WebSocket requires HTTP/1.1. Nginx defaults to HTTP/1.0 for proxied requests.
  2. proxy_set_header Upgrade $http_upgrade — passes the Upgrade: websocket header from the client to the upstream.
  3. proxy_set_header Connection "upgrade" — tells the upstream server to upgrade the connection.

Without these, nginx strips the Upgrade header and responds with a regular HTTP response instead of 101 Switching Protocols.

Pro Tip: Use a map block to handle both WebSocket and regular HTTP connections on the same location — this allows the same nginx config to work for both upgrade and non-upgrade requests:

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

server {
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

When $http_upgrade is empty (regular HTTP), Connection: close is set. When it is websocket, Connection: upgrade is set.

Fix 2: Increase Proxy Timeouts

Nginx’s default proxy_read_timeout is 60 seconds. An idle WebSocket connection with no data for 60 seconds gets dropped. Increase all relevant timeouts:

location /socket/ {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

    # Increase timeouts for long-lived WebSocket connections
    proxy_read_timeout 3600s;    # 1 hour
    proxy_send_timeout 3600s;    # 1 hour
    proxy_connect_timeout 10s;   # Connection establishment timeout (keep short)

    # Disable buffering for real-time data
    proxy_buffering off;
    proxy_cache off;
}

proxy_read_timeout is the timeout between two successive read operations from the upstream — for WebSocket, this means “how long to wait for the upstream to send data.” Set it higher than the longest expected idle period.

Alternatively, configure WebSocket ping/pong in your application to keep the connection alive:

// Node.js WebSocket server (ws library)
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 3000 });

wss.on("connection", function(ws) {
  // Send ping every 30 seconds
  const pingInterval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.ping();
    }
  }, 30000);

  ws.on("close", () => clearInterval(pingInterval));
});

Ping/pong keeps the connection alive and is more reliable than relying on nginx timeout settings.

Fix 3: Configure wss:// (WebSocket over SSL)

For HTTPS sites, WebSocket connections must use wss:// (WebSocket Secure). Configure nginx to handle SSL termination and proxy to an unencrypted backend:

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location /socket/ {
        proxy_pass http://localhost:3000;  # Backend uses plain ws://
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header 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 https;  # Tell backend the original protocol

        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

The client connects via wss://example.com/socket/. Nginx terminates SSL and proxies to http://localhost:3000 (plain WebSocket). The backend does not need to handle SSL.

Client-side connection:

// Automatically use wss:// on HTTPS pages
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const ws = new WebSocket(`${protocol}//${window.location.host}/socket/`);

Fix 4: Proxy WebSocket to a Specific Path or Port

If the WebSocket server runs on a different port or path than the HTTP server:

# Backend: HTTP on port 3000, WebSocket on port 3001
server {
    listen 80;
    server_name example.com;

    # Regular HTTP requests
    location / {
        proxy_pass http://localhost:3000;
    }

    # WebSocket connections to a different backend port
    location /ws/ {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }
}

If the WebSocket path needs to be rewritten:

location /socket/ {
    # Strip /socket/ prefix before passing to backend
    proxy_pass http://localhost:3000/;  # Trailing slash rewrites the path
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

A trailing slash on proxy_pass strips the location prefix. /socket/chat becomes /chat at the backend.

Fix 5: Fix nginx Load Balancing with WebSockets

When proxying WebSockets to multiple upstream servers, connections must be sticky (each WebSocket connection stays with the same upstream server, since WebSocket is stateful):

upstream websocket_backends {
    ip_hash;  # Sticky sessions based on client IP
    server backend1:3000;
    server backend2:3000;
    server backend3:3000;
}

server {
    listen 80;

    location /socket/ {
        proxy_pass http://websocket_backends;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }
}

ip_hash ensures all requests from the same client IP go to the same upstream. For more precise stickiness (especially behind a NAT where many users share one IP), use sticky cookie from nginx Plus or a session-aware load balancer.

Alternative: use Redis pub/sub to share WebSocket messages across multiple server instances (Socket.IO supports this with socket.io-redis).

Fix 6: Debug WebSocket Connection Issues

Test the upstream WebSocket server directly:

# Test WebSocket connection directly (bypassing nginx)
wscat -c ws://localhost:3000/socket
# If this fails, the issue is in your backend, not nginx

Install wscat:

npm install -g wscat

Test through nginx:

wscat -c ws://example.com/socket
# If direct works but through nginx fails, the issue is nginx config

Check nginx error logs:

sudo tail -f /var/log/nginx/error.log
# Look for: "no live upstreams", "connect() failed", "upstream timed out"

Test with curl to see the response headers:

curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  http://example.com/socket/

Expected response: HTTP/1.1 101 Switching Protocols. If you get 200 OK or 502 Bad Gateway, the WebSocket upgrade is not happening.

Enable nginx debug logging for a specific connection:

error_log /var/log/nginx/error.log debug;

Fix 7: Fix WebSocket with Docker and Compose

When nginx and the WebSocket backend run in Docker containers, use container names as upstream addresses:

# nginx.conf inside Docker
upstream websocket_backend {
    server app:3000;  # 'app' is the Docker service name
}

server {
    listen 80;

    location /socket/ {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }
}
# docker-compose.yml
services:
  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "80:80"
    depends_on:
      - app

  app:
    image: myapp:latest
    expose:
      - "3000"

Both services must be on the same Docker network (Compose creates a default network automatically).

Still Not Working?

Check for double Connection header. If another middleware or proxy is already setting Connection: close, it overrides your Connection: upgrade. Verify with curl -v that the upstream receives Connection: upgrade.

Check browser WebSocket error codes. The browser’s Network tab shows the WebSocket upgrade request. Status 101 means success. 400 Bad Request means the upstream rejected the upgrade. 502 Bad Gateway means nginx could not reach the upstream.

Check for HTTP/2 conflicts. HTTP/2 uses a different multiplexing mechanism and does not support WebSocket upgrades in the same way. If you have http2 in your nginx listen directive, WebSocket connections from HTTP/2 clients need special handling:

listen 443 ssl;  # Remove 'http2' if WebSocket is breaking
# Or use HTTP/2 and WebSocket on separate locations

For nginx 502 errors in general, see Fix: Nginx 502 Bad Gateway. For nginx location matching issues, see Fix: Nginx location block not matching.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles