Fix: Nginx WebSocket Proxy Not Working (101 Switching Protocols Failed)
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: 502Or:
WebSocket connection to 'ws://localhost/socket' failed:
Error during WebSocket handshake: net::ERR_CONNECTION_RESETOr the WebSocket connects initially but drops after exactly 60 seconds:
WebSocket connection closed unexpectedlyOr 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
UpgradeandConnectionheaders in the nginx proxy config — required for WebSocket handshake. proxy_http_versionnot 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_timeoutis 60 seconds, dropping idle WebSocket connections. - SSL termination misconfigured —
wss://(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:
proxy_http_version 1.1— WebSocket requires HTTP/1.1. Nginx defaults to HTTP/1.0 for proxied requests.proxy_set_header Upgrade $http_upgrade— passes theUpgrade: websocketheader from the client to the upstream.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_upgradeis empty (regular HTTP),Connection: closeis set. When it iswebsocket,Connection: upgradeis 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 nginxInstall wscat:
npm install -g wscatTest through nginx:
wscat -c ws://example.com/socket
# If direct works but through nginx fails, the issue is nginx configCheck 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 locationsFor nginx 502 errors in general, see Fix: Nginx 502 Bad Gateway. For nginx location matching issues, see Fix: Nginx location block not matching.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Certbot Certificate Renewal Failed (Let's Encrypt)
How to fix Certbot certificate renewal failures — domain validation errors, port 80 blocked, nginx config issues, permissions, and automating renewals with systemd or cron.
Fix: Kubernetes Ingress Not Working (404, 502, or Traffic Not Routing)
How to fix Kubernetes Ingress not routing traffic — why Ingress returns 404 or 502, how to configure annotations correctly, debug ingress-nginx and AWS ALB Ingress Controller, and verify backend service health.
Fix: Socket.IO Not Connecting (CORS, Transport, and Namespace Errors)
How to fix Socket.IO connection failures — CORS errors, transport fallback issues, wrong namespace, server not emitting to the right room, and how to debug Socket.IO connections in production.
Fix: EMFILE Too Many Open Files / ulimit Error on Linux
How to fix EMFILE too many open files errors on Linux and Node.js — caused by low ulimit file descriptor limits, file handle leaks, and how to increase limits permanently.