WebSocket traffic starts as an HTTP request and then upgrades into a long-lived tunnel. A working HAProxy configuration must preserve the upgrade handshake and keep the upgraded connection open longer than an ordinary short HTTP request.
HAProxy can route WebSocket requests in HTTP mode because the initial handshake uses HTTP headers such as Connection and Upgrade. After the backend returns 101 Switching Protocols, timeout tunnel controls how long HAProxy allows the upgraded stream to stay idle.
Set WebSocket timeouts deliberately. A normal HTTP server timeout can close quiet WebSocket clients too early, while an unlimited tunnel timeout can hold resources long after clients disappear.
Steps to configure HAProxy for WebSocket traffic:
- Confirm the WebSocket backend is reachable from the HAProxy host.
$ nc -zv 10.0.30.11 8080 Connection to 10.0.30.11 8080 port [tcp/*] succeeded!
A TCP connect check proves only reachability. The upgrade test later proves that the backend accepts WebSocket handshakes through HAProxy.
- Open the active HAProxy configuration file.
$ sudoedit /etc/haproxy/haproxy.cfg
- Add a frontend route and WebSocket backend.
- /etc/haproxy/haproxy.cfg
frontend fe_http bind :80 mode http acl is_websocket hdr(Upgrade) -i websocket use_backend be_websocket if is_websocket default_backend be_http backend be_http server app1 10.0.10.11:8080 check backend be_websocket mode http option http-server-close timeout tunnel 1h server ws1 10.0.30.11:8080 check
hdr(Upgrade) -i websocket matches the upgrade request. timeout tunnel applies after the connection switches protocols.
- Set maxconn in frontend fe_http to the peak number of WebSocket clients.
- /etc/haproxy/haproxy.cfg
frontend fe_http bind :80 mode http maxconn 20000
Long-lived sockets consume file descriptors, memory, and backend capacity even when they are idle. Each client holds one socket to the client and one to the backend, so 20000 clients need about 40000 file descriptors. An idle tunnel closes when timeout tunnel expires, so the value must exceed the gap between the application's ping or heartbeat messages.
Related: How to configure connection limits in HAProxy
Tool: WebSocket Connection Capacity Calculator - Validate the complete HAProxy configuration.
$ sudo haproxy -c -V -f /etc/haproxy/haproxy.cfg Configuration file is valid
- Reload HAProxy after validation succeeds.
$ sudo systemctl reload haproxy
- Send a raw WebSocket upgrade probe through HAProxy.
$ printf 'GET /ws HTTP/1.1\r\nHost: ws.example.net\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n' | nc -w 2 www.example.net 80 HTTP/1.1 101 Switching Protocols upgrade: websocket connection: Upgrade sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= x-backend: ws1
HTTP 101 proves the request reached a WebSocket-capable backend and the upgrade headers survived the HAProxy route.
- Check ordinary HTTP traffic still reaches the default backend.
$ curl -sS -i http://www.example.net/ HTTP/1.1 200 OK plain HTTP backend
- Check the HAProxy log for the closed WebSocket connection.
$ sudo grep be_websocket /var/log/haproxy.log 2026-11-07T06:03:36.939104+00:00 haproxy1 haproxy[3009]: 127.0.0.1:42916 [07/Nov/2026:06:03:34.909] fe_http be_websocket/ws1 0/0/1/14/2029 101 144 - - ---- 1/1/0/0/0 0/0 "GET /ws HTTP/1.1"
HAProxy writes this line when the connection closes, so a client that is still connected does not appear yet. be_websocket/ws1 and status 101 show the upgrade reached the WebSocket server, and cD in place of ---- means an idle timeout closed the connection.
Related: How to configure HAProxy loggingIf WebSocket clients disconnect at a fixed interval, compare timeout client, timeout server, timeout tunnel, backend idle timeout, and any firewall or CDN idle timeout in front of HAProxy.