Nginx WebSocket Reverse Proxy: Complete Configuration Guide

Topic: nginx-proxy-pass-websocket-upgradeUpdated 7/29/2026

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 /ws block (or equivalent WebSocket path) to enable WebSocket protocol upgrade through Nginx.
  • Production timeout: Set proxy_read_timeout 7d and proxy_send_timeout 7d to prevent premature disconnection of long-lived WebSocket connections (default 60 seconds is too short).
  • Load balancing: Use ip_hash directive 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:

NGINX
http {
    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)

ParameterValuePurpose
proxy_http_version1.1WebSocket upgrade requires HTTP/1.1 (HTTP/1.0 does not support upgrade)
proxy_set_header Upgrade$http_upgradePasses 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)

ParameterRecommended ValuePurpose
proxy_read_timeout7dPrevents Nginx from closing idle WebSocket connections (default 60s)
proxy_send_timeout7dSame as above for send direction
proxy_connect_timeout7dAllows long connection establishment for slow clients
proxy_bufferingoffReduces latency by disabling response buffering
proxy_request_bufferingoffReduces latency by disabling request buffering
tcp_nodelayonDisables Nagle's algorithm for lower latency

Load Balancing Parameters

ParameterContextPurpose
ip_hashupstream blockEnsures same client IP routes to same backend (session stickiness)
least_connupstream blockRoutes to backend with fewest active connections
max_failsserver directiveNumber of failed attempts before marking backend as down (passive health check)
fail_timeoutserver directiveTime window for max_fails counting and backend recovery
keepaliveupstream blockConnection pool size for upstream keepalive (improves performance)

SSL Parameters

ParameterPurpose
ssl_certificatePath to SSL certificate file (e.g., /etc/ssl/certs/fullchain.pem)
ssl_certificate_keyPath to SSL private key file (e.g., /etc/ssl/private/privkey.pem)
ssl_protocolsAllowed TLS versions (recommend TLSv1.2 TLSv1.3)
ssl_ciphersAllowed 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_timeout expires (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:

NGINX
proxy_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:

NGINX
proxy_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:

  1. Verify certificate chain is complete: openssl s_client -connect ws.example.com:443 -showcerts
  2. Check file permissions: chmod 644 /etc/ssl/certs/fullchain.pem && chmod 600 /etc/ssl/private/privkey.pem
  3. Ensure ssl_protocols includes TLSv1.2 or TLSv1.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:

NGINX
if ($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_connections and system file descriptors. Adjust worker_rlimit_nofile and ulimit -n accordingly.
  • Connection pool: Use keepalive in upstream block to reuse backend connections:
    NGINX
    upstream websocket_backend {
        ip_hash;
        keepalive 32;
        server backend1:8080;
        server backend2:8080;
    }
    

Security Hardening

  1. Restrict WebSocket path: Only expose the specific WebSocket endpoint (e.g., /ws) rather than proxying all paths
  2. 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;
    
  3. 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;
    
  4. 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;
    
  5. 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:

NGINX
server {
    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.

Related Guides