Proxy secure WebSocket connections through Nginx

Configure WSS termination, HTTP upgrade headers, idle timeouts, and connection tests without confusing a handshake with a working message stream.

Best practices
Illustration of server operations and log analysis

To proxy a conventional HTTP/1.1 WebSocket upgrade, explicitly forward Upgrade and Connection and use HTTP/1.1 to the backend. Configure a TLS certificate on Nginx so clients can connect using wss://.

Configure the TLS endpoint and upgrade headers

Place the map and server inside the existing http context:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    server_name ws.example.com;
    ssl_certificate     /etc/nginx/ssl/ws.example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/ws.example.com.key;

    location /ws/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        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_read_timeout 120s;
        proxy_send_timeout 120s;
    }
}

Replace the endpoint and certificate paths with your deployment's values. With no URI suffix in proxy_pass, /ws/ remains part of the upstream request path.

The map sends Connection: close for requests without an Upgrade header; it does not send an empty value. Upgrade headers are hop-by-hop and must be forwarded deliberately. The explicit version works across releases: before Nginx 1.29.7 the default upstream HTTP version was 1.0; from 1.29.7 it is 1.1.

TLS here protects the client-to-Nginx hop. For an untrusted upstream network, use proxy_pass https://... and configure upstream certificate verification, trust, and SNI as well.

Keep idle connections intentional

proxy_read_timeout limits silence between upstream reads, not total session lifetime. The default is 60 seconds. Choose a timeout and server heartbeat interval that fit the application and any earlier load balancer. A server ping frame can reset the read timer; traffic only from the client does not necessarily do so.

A longer timeout does not replace detecting failed peers. Test quiet periods, bidirectional messages, reconnects, and authentication expiry.

Check the handshake, then actual messages

curl --http1.1 -i -N --max-time 10 \
  -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
  -H 'Sec-WebSocket-Version: 13' \
  -H 'Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==' \
  https://ws.example.com/ws/

A valid upgrade should return 101 Switching Protocols. This curl example is only a bounded handshake check; its timeout is expected if the connection stays open. Use a WebSocket client to test frames and sustained operation.

DataKit log collection can capture handshake and proxy-error records. Application disconnect reasons and message latency need application instrumentation; HTTP 499/502 counts are not a complete WebSocket disconnect metric.

FAQ

Why do I get 400 or 426?

Check upgrade headers, the selected location, backend path, Origin restrictions, authentication, and subprotocol requirements. The status alone does not establish which one failed.

Why does it disconnect after about 60 seconds?

Check Nginx's read timeout and every other intermediary's idle timeout. Verify that server heartbeats actually reach the client before changing limits.

Do multiple backends require sticky sessions?

An established WebSocket stays on its chosen backend. Reconnects may select another instance. Shared state or cross-instance messaging can support that; affinity may help a particular application, but ip_hash is not universally required and behaves poorly for many clients behind one address.

Reference: Nginx WebSocket proxying.

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting