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.