Reverse proxy applications

Proxy WebSockets and streams

Forward protocol upgrades and long-lived responses without applying ordinary request assumptions blindly.

8 minute lesson

~~~

WebSocket connections begin as HTTP and then upgrade to a different protocol on the same connection. The client sends a normal-looking request with Upgrade: websocket and Connection: Upgrade headers, the server answers 101 Switching Protocols, and from that moment the connection carries WebSocket frames instead of HTTP.

A default proxy configuration breaks this in two ways. Nginx speaks HTTP/1.0 to upstreams unless told otherwise, and HTTP/1.0 has no upgrade mechanism. And Upgrade is a hop-by-hop header, so Nginx doesn’t forward it on its own. You opt in per location:

location /ws/ {
  proxy_pass http://127.0.0.1:4000;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_read_timeout 300s;
}

proxy_http_version 1.1 makes the upstream leg capable of upgrading. The two proxy_set_header lines pass the client’s Upgrade value through and force Connection to request the upgrade.

The proxy_read_timeout matters more here than anywhere else. For a WebSocket it counts the time between two reads on an established connection, so a quiet-but-healthy socket gets closed by Nginx after the timeout — the default is only 60 seconds. Raise it for the WebSocket location, or make the application send periodic pings inside the timeout window.

Verify the handshake with curl:

curl -i http://chat.example.com/ws/ \
  -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
  -H "Sec-WebSocket-Version: 13"
# HTTP/1.1 101 Switching Protocols
# Upgrade: websocket
# Connection: upgrade

The status to look for is 101 with the upgrade headers echoed back. If you get a 200 or a 400 from the application instead, the upgrade headers didn’t survive the proxy — almost always a missing proxy_http_version 1.1 or a location that overrode proxy_set_header and dropped the pair.

Server-sent events and other long-lived streaming responses share part of this story. They stay ordinary HTTP, so no upgrade headers, but they need proxy_buffering off; in the location and the same deliberate read timeout, or events pile up in Nginx’s buffer while the browser sees nothing.

Two operational notes. Capacity planning must include connections that stay open — a thousand idle WebSockets still occupy a thousand connections. And during a reload, old workers keep already-established sockets alive until they close, so long-lived connections delay old workers from exiting. Watch what happens to an open connection while you reload.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →