WebSockets provide full-duplex communication channels over a single TCP connection, enabling real-time features such as live chat, push notifications, collaborative editing, gaming, and live dashboards. Unlike standard HTTP, which is request-response, WebSocket connections are persistent and bidirectional — the server can push data to clients at any time without a client request. Nginx can act as a reverse proxy for WebSocket backends, handling the HTTP Upgrade handshake that converts a standard HTTP/1.1 connection into a WebSocket connection. This guide covers configuring Nginx on RHEL 9 to proxy WebSocket traffic to a Node.js or Python backend, handling the Upgrade headers correctly, configuring timeouts for long-lived connections, and testing with wscat.

Prerequisites

  • Nginx installed on RHEL 9
  • A WebSocket backend server running on a local port (e.g., Node.js ws server on port 3000)

Step 1 — Configure Nginx WebSocket Proxy

WebSocket proxying requires forwarding the Upgrade and Connection headers, and extending proxy timeouts since WebSocket connections can stay open for hours:

# /etc/nginx/conf.d/websocket.conf
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen      80;
    server_name ws.example.com;

    # HTTP health check endpoint (non-WebSocket)
    location /health {
        return 200 "OK";
    }

    # WebSocket proxy endpoint
    location /ws/ {
        proxy_pass         http://localhost:3000;
        proxy_http_version 1.1;

        # These two headers are critical for the WebSocket Upgrade handshake
        proxy_set_header   Upgrade    $http_upgrade;
        proxy_set_header   Connection $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;

        # Extended timeouts — WebSocket connections are long-lived
        proxy_read_timeout    3600s;  # 1 hour
        proxy_send_timeout    3600s;
        proxy_connect_timeout 10s;

        # Disable buffering for real-time streaming
        proxy_buffering     off;
        proxy_cache         off;
    }
}
nginx -t && systemctl reload nginx

Step 2 — Mixed HTTP and WebSocket on Same Server

server {
    listen      443 ssl http2;
    server_name app.example.com;

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

    # Standard REST API
    location /api/ {
        proxy_pass http://localhost:4000;
    }

    # WebSocket upgrade path
    location /socket.io/ {
        proxy_pass         http://localhost:4000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade    $http_upgrade;
        proxy_set_header   Connection $connection_upgrade;
        proxy_set_header   Host       $host;
        proxy_read_timeout 86400s;
    }

    # Static files
    location / {
        root /var/www/app;
        try_files $uri /index.html;
    }
}

Step 3 — WebSocket Load Balancing

# WebSocket connections must use ip_hash to ensure a client always
# hits the same backend (session affinity)
upstream websocket_servers {
    ip_hash;   # Sticky sessions — required for WebSocket state
    server 10.0.1.10:3000;
    server 10.0.1.11:3000;
    server 10.0.1.12:3000;
}

Step 4 — Test WebSocket Connection

# Install wscat (Node.js WebSocket client)
dnf install -y nodejs npm
npm install -g wscat

# Connect to the WebSocket endpoint
wscat -c ws://ws.example.com/ws/

# With SSL
wscat -c wss://app.example.com/socket.io/

# Verify Upgrade header is returned
curl -s -I http://ws.example.com/ws/ -H "Upgrade: websocket" -H "Connection: Upgrade" | grep -i upgrade

Conclusion

Nginx WebSocket proxying on RHEL 9 requires only three additional directives — proxy_http_version 1.1, Upgrade, and Connection headers — plus extended timeouts for persistent connections. The map directive elegantly handles both WebSocket upgrade requests and regular HTTP connections on the same location block. When load balancing WebSocket backends, ip_hash ensures connection affinity so stateful sessions are maintained across multiple backend servers.

Next steps: How to Harden Nginx: Security Headers, TLS 1.3 and OCSP Stapling on RHEL 9, How to Monitor Nginx with Prometheus on RHEL 9, and How to Configure Nginx Rate Limiting on RHEL 9.