Nginx WebSocket proxying breaks when a normal Nginx reverse proxy handles regular HTTP requests but does not pass the upgrade handshake to the backend. The page may load, while the browser console shows a failed ws:// or wss:// connection, or the server returns 400, 502, or 504 on the WebSocket path.
A reliable Nginx WebSocket proxy uses the standard map pattern, upgrade headers, compatibility-safe upstream protocol settings, timeout tuning, and direct handshake checks. The pattern fits Node.js, Socket.IO, Python, Go, dashboards, chat applications, and any backend that already accepts WebSocket connections on a local or private address.
Understand Nginx WebSocket Proxying
WebSocket starts as an HTTP request, then switches protocols after a successful upgrade handshake. Nginx can proxy that handshake, but the WebSocket location needs a few settings that a basic HTTP reverse proxy might not include.
| Directive or Header | Context | Purpose |
|---|---|---|
proxy_http_version 1.1 | http, server, location | Keeps upstream proxying compatible with Nginx builds that still require HTTP/1.1 for WebSocket upgrade tunneling. |
Upgrade $http_upgrade | location | Passes the client upgrade request to the backend application. |
Connection $connection_upgrade | location | Uses a mapped value so WebSocket requests send upgrade while ordinary HTTP requests can send close. |
map | http | Creates the $connection_upgrade variable used by shared HTTP and WebSocket locations. |
proxy_read_timeout | http, server, location | Controls how long Nginx waits between reads from the upstream connection. |
The Nginx WebSocket proxying documentation uses this same upgrade-header pattern. Upstream examples mark proxy_http_version 1.1 as needed before Nginx 1.29.7, so keeping the directive in portable examples avoids surprises on older servers. The map version is safer than hardcoding Connection "upgrade" when the same location can receive both WebSocket and ordinary HTTP requests.
Only configure the origin Nginx layer you control. If Cloudflare, a load balancer, Nginx Proxy Manager, Kubernetes Ingress, Plesk, cPanel, or another proxy owns the public edge, that layer may also need WebSocket support, timeout changes, or path routing.
Check the WebSocket Backend Before Editing Nginx
Confirm the backend already listens from the Nginx server. Replace the port and path with your application endpoint:
curl -i http://127.0.0.1:3000/health
A normal health endpoint might return a simple 200 response:
HTTP/1.1 200 OK
Content-Type: application/json
{"status":"ok"}
If the direct backend request fails, fix the application listener before changing Nginx. A WebSocket proxy cannot repair a service that is stopped, listening on another address, blocked by a container network, or bound only inside a different namespace.
Configure an Nginx WebSocket Proxy
These examples use 127.0.0.1:3000 for the upstream application and example.com for the public hostname. Replace both values before publishing the config on a real server.
Choose the Nginx WebSocket Configuration File
Nginx package layouts vary. Debian and Ubuntu commonly use /etc/nginx/sites-available/ with symlinks under /etc/nginx/sites-enabled/. Fedora, RHEL, Rocky Linux, many nginx.org packages, and several control-panel setups commonly load site files from /etc/nginx/conf.d/.
Check which include paths your active Nginx config loads:
sudo nginx -T 2>/dev/null | grep -E '^[[:space:]]*include[[:space:]].*(sites-enabled|conf\.d)'
Relevant output usually shows one or both include locations. If the command returns no include path, inspect the active http block before choosing where to place the site file.
include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*;
Back up the main Nginx configuration before editing nginx.conf or replacing an existing production site file:
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.backup
On Debian and Ubuntu, create a site file under sites-available:
sudo nano /etc/nginx/sites-available/websocket-proxy
On conf.d-based layouts, create a .conf file instead:
sudo nano /etc/nginx/conf.d/websocket-proxy.conf
Add the Nginx WebSocket Map in the HTTP Context
Add this map block in the http context, outside every server block. A site file works when the active include path loads it from the http context, such as sites-enabled or conf.d. If neither include path exists, place the map directly inside the http block in /etc/nginx/nginx.conf.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
The empty-string line handles normal HTTP requests that do not send an Upgrade header. That makes the same proxy location usable for a web application that serves both normal pages and WebSocket connections.
Add the Nginx WebSocket Proxy Server Block
Use an upstream block for the backend, then pass upgrade headers inside the WebSocket location. This example proxies the whole site to the backend application:
upstream websocket_backend {
server 127.0.0.1:3000;
}
server {
listen 80;
listen [::]:80;
server_name example.com;
location / {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
If your backend expects a specific WebSocket path, keep the path stable and avoid changing it accidentally with a trailing slash in proxy_pass. The Nginx proxy_pass trailing slash guide explains when Nginx replaces the matched location prefix and when it passes the original URI unchanged.
On Debian and Ubuntu systems, enable the site after saving the file:
sudo ln -s /etc/nginx/sites-available/websocket-proxy /etc/nginx/sites-enabled/
Skip the symlink step on layouts that load /etc/nginx/conf.d/*.conf directly.
Test and Reload the Nginx WebSocket Proxy
Run the Nginx syntax test before applying the proxy:
sudo nginx -t
A successful syntax test prints these lines:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
On systemd-based Linux hosts, reload Nginx only after the syntax check passes:
sudo systemctl reload nginx
The reload command normally returns no output on success. If it fails, inspect the service logs with journalctl and the Nginx error log before changing the proxy again.
Verify the Nginx WebSocket Proxy
Start with a normal GET request through Nginx while printing only the response headers. This confirms that the virtual host, DNS or host header, and upstream routing work before you test the WebSocket handshake:
curl --silent --show-error --max-time 10 -o /dev/null -D - -H "Host: example.com" http://127.0.0.1/
A working application usually returns a normal HTTP response:
HTTP/1.1 200 OK Server: nginx
Then test the WebSocket handshake path through the local Nginx listener. Replace example.com and /socket with the virtual host and endpoint your application uses. The short timeout prevents a successful upgraded connection from keeping the terminal open, while the filter prints only the response headers:
{ curl --http1.1 --no-progress-meter -i -N --max-time 5 \
-H "Host: example.com" \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
-H "Sec-WebSocket-Version: 13" \
http://127.0.0.1/socket 2>/dev/null || true; } | tr -d '\r' | sed -n '1,/^$/p'
A backend that accepts the handshake should return a switching-protocols response before the timeout ends:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: upgrade
The 101 response is the success signal; the timeout only closes the manual test connection after the header check. If no HTTP status appears, rerun the probe without 2>/dev/null to expose the curl connection error. Some applications require cookies, tokens, an Origin header, or an exact path before accepting a WebSocket handshake. If this command returns 400 or 403, check the application logs as well as Nginx logs. The result may mean the proxy worked but the application rejected the handshake.
Configure Nginx WebSocket Paths
Many applications serve normal HTTP routes and WebSocket routes from different paths. Keep the WebSocket location specific enough that static-file, PHP, or fallback rules do not intercept it first. The Nginx location block priority guide is the deeper reference when more than one location could match the same URI.
For Socket.IO, a common path-specific proxy looks like this:
location ^~ /socket.io/ {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_buffering off;
}
The ^~ modifier tells Nginx to stop checking later regex locations after this prefix wins. That helps when a separate regex location handles static assets, PHP, or file extensions.
If the WebSocket backend expects the path without the public prefix, change the URI deliberately rather than guessing. For example, proxying public /ws/ to upstream / is a proxy_pass URI-replacement problem, not a WebSocket-specific problem.
Configure Secure WebSocket Proxying with WSS
wss:// is WebSocket over HTTPS. In most deployments, Nginx terminates TLS for the browser and proxies to a private backend over HTTP. Keep the WebSocket proxy headers inside the HTTPS server block just as you would for the HTTP block.
server {
listen 443 ssl;
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 / {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
Redirect plain HTTP to HTTPS only after the HTTPS site works:
server {
listen 80;
listen [::]:80;
server_name example.com;
return 301 https://$host$request_uri;
}
Keep proxy_http_version 1.1 in the proxy location even when normal browser traffic uses HTTPS and HTTP/2 at the public edge. That directive controls the upstream connection from Nginx to the application, not only the browser connection to Nginx.
Tune Nginx WebSocket Timeouts and Buffering
WebSocket connections often stay open longer than ordinary requests. If the connection drops after a predictable idle period, tune the proxy timeouts and check whether the application sends heartbeat messages.
location /socket {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_connect_timeout 60s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_buffering off;
}
Do not hide application heartbeat problems with extremely large timeout values. A healthy WebSocket application should send pings, pongs, or other heartbeat traffic often enough that intermediaries do not treat the connection as idle forever.
Secure an Nginx WebSocket Proxy
WebSocket traffic can carry authenticated sessions, tokens, live dashboards, chat messages, shell consoles, or admin data. Treat the WebSocket endpoint as part of the application security boundary, not as a harmless static route.
- Validate allowed origins in the application when browser clients use cookies or other ambient authentication.
- Keep the backend bound to
127.0.0.1or a private address unless it must be reachable from another host. - Trust
X-Forwarded-ForandX-Forwarded-Protoonly from known proxy addresses inside the application. - Use TLS at the public edge for authenticated WebSocket traffic so browsers connect with
wss://. - Rate limit login, token refresh, or session-creation HTTP endpoints separately from long-lived WebSocket streams.
If the same host also serves normal web traffic, consider pairing the proxy setup with Nginx security headers and Nginx rate limiting where those controls fit the HTTP side of the application.
Troubleshoot Common Nginx WebSocket Proxy Errors
Use logs before changing several directives at once. The Nginx access and error logs guide explains how to inspect status codes, upstream errors, and per-site log files without relying on browser messages alone.
| Symptom | Likely Cause | First Check |
|---|---|---|
| Browser says WebSocket connection failed | Wrong URL, missing upgrade headers, blocked edge proxy, or rejected application handshake | Run the manual curl handshake and check backend logs. |
| 400 Bad Request | Backend rejected the handshake, path, token, or origin | Confirm the exact WebSocket path and required headers. |
| 502 Bad Gateway | Backend stopped, wrong upstream port, failed container mapping, or bad socket | Test the backend directly from the Nginx host. |
| 504 Gateway Timeout | Backend accepted the connection but did not respond before a timeout | Check proxy_read_timeout, app heartbeat, and upstream logs. |
| Connection drops after one minute | Idle timeout at Nginx, CDN, load balancer, or application layer | Compare Nginx timeouts with every upstream proxy in front of the app. |
Fix Missing Nginx WebSocket Upgrade Headers
If normal pages load but WebSocket requests fail, confirm the location contains all three upgrade essentials:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
If $connection_upgrade is undefined, the map block is missing or placed in the wrong context. Move the map block to the http context, then test Nginx again.
Fix 400 Bad Request on the Nginx WebSocket Path
A 400 response often comes from the application, not Nginx. Confirm the public path matches the backend path and that the application accepts the request without missing cookies, tokens, protocol parameters, or origin checks.
{ curl --http1.1 --no-progress-meter -i -N --max-time 5 \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
-H "Sec-WebSocket-Version: 13" \
http://127.0.0.1:3000/socket 2>/dev/null || true; } | tr -d '\r' | sed -n '1,/^$/p'
If the direct backend request fails the same way, fix the application route or authentication requirements first. If the backend accepts the request directly but fails through Nginx, inspect the matching location, proxy_pass URI handling, and forwarded headers.
Fix 502 Bad Gateway on an Nginx WebSocket Proxy
A 502 means Nginx could not successfully talk to the upstream. Check whether the backend process is running and listening on the exact address in proxy_pass or the upstream block:
ss -tuln 'sport = :3000'
Relevant output should show a listener on the expected address:
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port tcp LISTEN 0 511 127.0.0.1:3000 0.0.0.0:*
If Nginx logs show connection failures, use the Nginx 502 Bad Gateway troubleshooting guide to check upstream ports, sockets, service state, and container networking.
Fix 504 Gateway Timeout or Idle Disconnects
A 504 or predictable idle disconnect usually points to a timeout in Nginx, the application, a CDN, or a load balancer. Start by checking the Nginx error log:
sudo tail -n 50 /var/log/nginx/error.log
Relevant log fragments may include upstream timeout patterns:
upstream timed out while reading response header from upstream upstream timed out while reading upstream
Increase proxy_read_timeout only after checking application heartbeat behavior. After changing timeout values, run sudo nginx -t, reload Nginx, and repeat the WebSocket handshake test.
Fix Wrong Scheme, Host, or Client IP in the WebSocket App
If the application generates ws:// URLs on an HTTPS site, records every client as 127.0.0.1, or redirects to the wrong hostname, check the forwarded headers and the application proxy-trust settings:
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;
Forwarding the headers is only half of the fix. The backend must also be configured to trust headers from the Nginx proxy and reject forged values from direct clients.
Roll Back Nginx WebSocket Proxy Changes
Remove only the article-created file or symlink. Do not delete a production virtual host unless you know it belongs to this WebSocket proxy test.
For Debian and Ubuntu sites-enabled layouts, disable the site symlink:
sudo rm -f /etc/nginx/sites-enabled/websocket-proxy
For conf.d-based layouts, remove the article-created proxy file:
sudo rm -f /etc/nginx/conf.d/websocket-proxy.conf
If you created a separate map file and no other site uses it, remove that file too:
sudo rm -f /etc/nginx/conf.d/websocket-map.conf
On systemd-based Linux hosts, test and reload after cleanup:
sudo nginx -t && sudo systemctl reload nginx
If this site was replacing an older server block, restore the backup or re-enable the previous site file before reloading Nginx.
Conclusion
Nginx is proxying WebSocket traffic correctly when the backend responds directly, Nginx passes upgrade headers, syntax tests pass, and the handshake returns 101 through the server block. Keep the map block in the http context, retain proxy_http_version 1.1 for compatibility with older builds, and use logs plus direct backend probes when browser errors blur proxy and application failures.


Formatting tips for your comment
You can use basic HTML to format your comment. Useful tags currently allowed in published comments:
<code>command</code>command<strong>bold</strong><em>italic</em><blockquote>quote</blockquote>