Nginx WebSocket Reverse Proxy: Complete Configuration Guide
Quick Answer
- Core requirement: Nginx WebSocket proxying requires explicit HTTP/1.1 upgrade headers (
proxy_http_version 1.1,proxy_set_header Upgrade $http_upgrade,proxy_set_header Connection "upgrade") — without these, WebSocket connections fail with 400 Bad Request. - Minimal fix: Add the three mandatory directives inside your
location /wsblock (or equivalent WebSocket path) to enable WebSocket protocol upgrade through Nginx. - Production timeout: Set
proxy_read_timeout 7dandproxy_send_timeout 7dto prevent premature disconnection of long-lived WebSocket connections (default 60 seconds is too short). - Load balancing: Use
ip_hashdirective in the upstream block to ensure WebSocket sessions stick to the same backend server; without it, clients may experience 1006 Abnormal Closure on server rotation. - Version boundary: This configuration works with Nginx 1.3.13+ (WebSocket proxy support introduced in 1.3.13) and all modern Nginx versions including 1.24.x and 1.25.x.
What Problem It Solves
Nginx is a general-purpose HTTP reverse proxy, but WebSocket connections require an HTTP upgrade handshake (switching from HTTP to the WebSocket protocol). Without explicit configuration, Nginx treats WebSocket upgrade requests as regular HTTP requests, returning 400 Bad Request or failing to establish the persistent bidirectional connection. This guide provides the exact Nginx configuration to proxy WebSocket traffic reliably in production, including load balancing, SSL termination, timeout tuning, and security hardening.
Minimal Working Configuration
The following configuration enables WebSocket proxying through Nginx with the mandatory upgrade headers:
NGINXhttp { upstream websocket_backend { # Use ip_hash for session stickiness in production ip_hash; server backend1.example.com:8080 max_fails=3 fail_timeout=30s; server backend2.example.com:8080 max_fails=3 fail_timeout=30s; } server { listen 80; server_name ws.example.com; location /ws { # Mandatory WebSocket upgrade directives proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # Recommended headers for backend awareness 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; # Long-lived connection timeouts (7 days) proxy_read_timeout 7d; proxy_send_timeout 7d; proxy_connect_timeout 7d; # Disable buffering for real-time performance proxy_buffering off; proxy_request_buffering off; # TCP optimizations tcp_nodelay on; proxy_pass http://websocket_backend; } } }
Parameters and Environment Variables
Mandatory Parameters (Required for WebSocket)
| Parameter | Value | Purpose |
|---|---|---|
proxy_http_version | 1.1 | WebSocket upgrade requires HTTP/1.1 (HTTP/1.0 does not support upgrade) |
proxy_set_header Upgrade | $http_upgrade | Passes the client's Upgrade header to trigger WebSocket protocol switch |
proxy_set_header Connection | "upgrade" | Sets the Connection header to "upgrade" to maintain the WebSocket connection |
Recommended Parameters (Production Hardening)
| Parameter | Recommended Value | Purpose |
|---|---|---|
proxy_read_timeout | 7d | Prevents Nginx from closing idle WebSocket connections (default 60s) |
proxy_send_timeout | 7d | Same as above for send direction |
proxy_connect_timeout | 7d | Allows long connection establishment for slow clients |
proxy_buffering | off | Reduces latency by disabling response buffering |
proxy_request_buffering | off | Reduces latency by disabling request buffering |
tcp_nodelay | on | Disables Nagle's algorithm for lower latency |
Load Balancing Parameters
| Parameter | Context | Purpose |
|---|---|---|
ip_hash | upstream block | Ensures same client IP routes to same backend (session stickiness) |
least_conn | upstream block | Routes to backend with fewest active connections |
max_fails | server directive | Number of failed attempts before marking backend as down (passive health check) |
fail_timeout | server directive | Time window for max_fails counting and backend recovery |
keepalive | upstream block | Connection pool size for upstream keepalive (improves performance) |
SSL Parameters
| Parameter | Purpose |
|---|---|
ssl_certificate | Path to SSL certificate file (e.g., /etc/ssl/certs/fullchain.pem) |
ssl_certificate_key | Path to SSL private key file (e.g., /etc/ssl/private/privkey.pem) |
ssl_protocols | Allowed TLS versions (recommend TLSv1.2 TLSv1.3) |
ssl_ciphers | Allowed cipher suites (use modern, secure ciphers) |
Root Cause Analysis
WebSocket connections fail through Nginx because of the HTTP upgrade mechanism. When a client initiates a WebSocket connection, it sends an HTTP GET request with specific headers:
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Without proxy_http_version 1.1, Nginx downgrades to HTTP/1.0, which does not support the Upgrade mechanism. Without proxy_set_header Upgrade $http_upgrade and proxy_set_header Connection "upgrade", Nginx strips these critical headers, and the backend never receives the upgrade request — it sees a plain HTTP GET and responds with 400 Bad Request.
The 1006 Abnormal Closure error occurs when:
proxy_read_timeoutexpires (default 60 seconds) and Nginx closes the connection- Load balancing sends subsequent WebSocket frames to a different backend (session drift)
- Backend server crashes or restarts without Nginx detecting the failure
Common Errors and Fixes
Error: WebSocket connection fails with 400 Bad Request
Root cause: Missing or incorrect HTTP upgrade headers.
Fix: Verify these three directives are present in the WebSocket location block:
NGINXproxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
Error: WebSocket disconnects after 60 seconds (1006 Abnormal Closure)
Root cause: Default proxy_read_timeout (60s) closes idle connections.
Fix: Set timeouts to match your application's heartbeat interval or use 7d:
NGINXproxy_read_timeout 7d; proxy_send_timeout 7d;
Also ensure your application sends WebSocket ping/pong frames every 30-60 seconds to keep the connection alive.
Error: SSL handshake fails with certificate unknown
Root cause: Missing intermediate certificates or incorrect file permissions.
Fix:
- Verify certificate chain is complete:
openssl s_client -connect ws.example.com:443 -showcerts - Check file permissions:
chmod 644 /etc/ssl/certs/fullchain.pem && chmod 600 /etc/ssl/private/privkey.pem - Ensure
ssl_protocolsincludesTLSv1.2orTLSv1.3
Error: CORS preflight fails with missing Access-Control-Allow-Origin
Root cause: OPTIONS request not handled or incorrect CORS headers.
Fix: Add CORS handling in the WebSocket location:
NGINXif ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' 'https://yourdomain.com' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Sec-WebSocket-Protocol' always; add_header 'Access-Control-Max-Age' 1728000 always; return 204; }
Security note: Never use $http_origin in production — hardcode the allowed origin domain.
Production Notes and Security Checks
Performance Limits
- Concurrent connections: Limited by
worker_connectionsand system file descriptors. Adjustworker_rlimit_nofileandulimit -naccordingly. - Connection pool: Use
keepalivein upstream block to reuse backend connections:NGINXupstream websocket_backend { ip_hash; keepalive 32; server backend1:8080; server backend2:8080; }
Security Hardening
- Restrict WebSocket path: Only expose the specific WebSocket endpoint (e.g.,
/ws) rather than proxying all paths - SSL/TLS: Always enable HTTPS and disable insecure protocols:
NGINX
ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers on; - Security headers:
NGINX
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY; - Rate limiting: Protect against abuse:
NGINX
limit_req_zone $binary_remote_addr zone=websocket:10m rate=10r/s; limit_req zone=websocket burst=20 nodelay; - Monitoring: Enable detailed logging for WebSocket connections:
NGINX
log_format websocket '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_upgrade" "$http_connection" ' 'upstream: "$upstream_addr"'; access_log /var/log/nginx/websocket_access.log websocket;
Application Layer Heartbeat
The 7-day timeout configuration can lead to zombie connections if the backend crashes without Nginx detecting it. Implement application-level WebSocket ping/pong every 30-60 seconds. When a pong timeout occurs, the backend should close the connection, allowing Nginx to detect the failure and route to a healthy backend.
FAQ
Q: How do I enable HTTP/3 (QUIC) for WebSocket proxying?
A: HTTP/3 requires compiling Nginx with --with-http_v3_module (using quiche or boringssl). Configuration example:
NGINXserver { listen 443 quic reuseport; http3 on; add_header Alt-Svc 'h3=":443"; ma=86400'; # WebSocket location remains the same }
Note: WebSocket over HTTP/3 uses RFC 9220 extended CONNECT. Both client and server must support it. Nginx official binaries do not include QUIC — you must compile from source.
Q: What are the limitations of ip_hash for WebSocket load balancing?
A: ip_hash has three key limitations: (1) Client IP changes (mobile network switching, NAT) cause session drift to a different backend; (2) Adding or removing backends redistributes the hash, breaking all existing sessions; (3) Multiple clients behind the same NAT (corporate proxy) all hash to the same backend, causing uneven load. Alternatives include sticky cookies (Nginx Plus only), application-layer session routing (JWT/cookie-based), or external session stores (Redis).
Q: How do I monitor WebSocket proxy health and performance?
A: Use a combination of: (1) Custom log format capturing $http_upgrade and $upstream_addr fields; (2) error_log at debug level for specific locations during troubleshooting; (3) Prometheus exporter (nginx-prometheus-exporter) for metrics like active connections, request rates, and upstream response times; (4) Passive health checks via max_fails/fail_timeout; (5) Application-level WebSocket ping/pong with pong timeout logging.