Configure Tengine WebSocket reverse proxy
2026-09-29
Your chat or real-time feature works locally, but behind Tengine the WebSocket handshake fails with 400 Bad Request or disconnects every few seconds. The reason: WebSocket needs the HTTP Upgrade headers forwarded explicitly — a plain proxy_pass does not do that. Here is the config that fixes it, and the timeout that stopped my connections from dropping.
The minimum working config
location /ws/ {
proxy_pass http://backend_pool;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s;
}
The two lines that matter for WebSocket:
proxy_set_header Upgrade $http_upgrade;— forwards the client’s upgrade requestproxy_set_header Connection "upgrade";— tells the backend this is a connection upgrade, not a normal request
Why connections drop after a minute
The default proxy_read_timeout is 60 seconds. A WebSocket that idles longer than that gets cut by Tengine even though the connection is healthy. Raising it to an hour keeps long-lived sockets alive:
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
Common mistakes I have debugged
- Wrong location scope. Put this only on the WebSocket path, not on normal REST routes — forcing
Connection: upgradeeverywhere confuses regular HTTP clients. - Backend needs the right path. If your app serves sockets at
/wsand you proxy from/, the handshake URI mismatch gives a 400. Match the location to what the backend expects. - Not checking the error log. A 502 during handshake usually means the backend is not listening on the socket path at all — see my 502 guide for the log-reading steps.
Verify the handshake
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
https://yourdomain.com/ws/
A successful response starts with 101 Switching Protocols.
Summary
Forward Upgrade and Connection: upgrade, raise proxy_read_timeout, and scope it to the socket path. If handshakes still fail, the backend side is the next suspect. And if your upstream is load-balanced, my load balancer guide shows how to keep sticky sockets working.