Understanding WebSocket Proxying in HAProxy
WebSocket is a protocol that starts its life as an ordinary HTTP/1.1 request and then upgrades to a persistent, full-duplex binary stream over the same TCP connection. For load balancers, this creates a fundamental challenge: the connection must be treated as HTTP long enough to inspect headers and route it to the correct backend, then handed off as a raw TCP tunnel for the remainder of its lifetime — which could be hours or days.
HAProxy handles WebSocket natively through its HTTP mode with automatic tunnel detection. Once HAProxy forwards the HTTP 101 Switching Protocols response from the backend, it enters tunnel mode on that connection and stops parsing the byte stream as HTTP. The key insight is that all routing decisions, header injection, and ACL evaluation happen exclusively during the initial HTTP handshake phase. After the 101 response, HAProxy is a transparent pipe. This article covers everything required to make that pipeline production-ready: detecting upgrades, keeping connections alive, routing traffic to dedicated WebSocket backend pools, enforcing stickiness, validating backend health, and limiting abuse.
The WebSocket Handshake: What HAProxy Sees
When a WebSocket client connects, it sends a standard HTTP GET request with specific upgrade headers. HAProxy evaluates this request through all configured ACLs and rules before forwarding it to the selected backend:
GET /ws/chat HTTP/1.1
Host: ws.solvethenetwork.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
The backend responds with HTTP 101 Switching Protocols:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
The moment HAProxy forwards this 101 response to the client, it transitions the connection to tunnel mode. All subsequent bytes in both directions are passed through without inspection or modification. If HAProxy were to attempt HTTP parsing after the upgrade, the WebSocket frame format would cause immediate parse errors and the connection would be terminated. HAProxy avoids this automatically, but your timeout and ACL configuration must be designed with this behavior in mind.
HAProxy Timeout Behavior and WebSocket
The most common production failure with WebSocket and HAProxy is silent connection termination caused by timeouts. The default timeout values in HAProxy are designed for short-lived HTTP transactions and will aggressively close idle WebSocket connections. Understanding which timeout applies at each phase is essential.
- timeout connect: Maximum time allowed to establish a TCP connection to the backend. Applies only during the initial TCP handshake. Does not affect established WebSocket connections.
- timeout client: Inactivity timeout on the client-facing side. Applies during the HTTP upgrade phase. After the upgrade, it is superseded by
timeout tunnel
if that directive is set. - timeout server: Inactivity timeout on the backend-facing side. Same behavior as
timeout client
— superseded bytimeout tunnel
once the connection enters tunnel mode. - timeout tunnel: Inactivity timeout specifically for tunneled connections, including WebSocket and HTTP CONNECT tunnels. This overrides both
timeout client
andtimeout server
once the 101 response is exchanged. If this directive is absent, HAProxy falls back totimeout client
andtimeout server
.
Without an explicit
timeout tunnel, a WebSocket connection that goes idle for longer than
timeout client(commonly set to 30s or 60s) will be silently dropped by HAProxy. Applications that use WebSocket for real-time notifications, dashboards, or interactive tools can have arbitrarily long idle periods between messages. The tunnel timeout should be set to a value that exceeds your application's maximum expected idle interval plus a safety margin. For most deployments,
3600sis a reasonable starting point. Long-running streaming connections may warrant
86400sor higher. Pair this with TCP keepalive at the OS level (
net.ipv4.tcp_keepalive_time) to detect truly dead connections without relying solely on HAProxy timeouts.
Minimum Viable WebSocket Configuration
The following configuration is the minimum required to proxy WebSocket traffic through HAProxy. SSL is terminated at the frontend, and all traffic — both standard HTTP and WebSocket upgrades — is passed to the same backend pool. The critical addition over a standard HTTP proxy is
timeout tunnel:
global
log /dev/log local0
log /dev/log local1 notice
maxconn 50000
user haproxy
group haproxy
daemon
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
timeout tunnel 3600s
frontend wss_frontend
bind *:443 ssl crt /etc/haproxy/certs/solvethenetwork.com.pem
bind *:80
http-request redirect scheme https unless { ssl_fc }
default_backend ws_backends
backend ws_backends
balance leastconn
option http-server-close
server ws-01 10.10.10.11:8080 check
server ws-02 10.10.10.12:8080 check
server ws-03 10.10.10.13:8080 check
This works for WebSocket tunneling, but it has a significant limitation: WebSocket connections are stateful. Once a client upgrades to WebSocket and exchanges messages, its application state typically lives in memory on a specific backend server. When the client disconnects and reconnects — due to a network hiccup, a browser tab being backgrounded, or a mobile device switching between WiFi and cellular — HAProxy will route the new connection to whichever backend has the fewest active connections at that moment. If the new backend does not have the client's session state, the user will see an error or lose context. Sticky sessions solve this.
Sticky Sessions for WebSocket Connections
HAProxy provides several mechanisms for sticky sessions. Cookie-based stickiness is the most reliable for WebSocket because it persists across disconnections at the HTTP layer, before the upgrade occurs. On first connection, HAProxy inserts a routing cookie into the HTTP response. On all subsequent connections, HAProxy reads that cookie and routes the client back to the same backend server, even after a disconnect and reconnect cycle.
backend ws_backends
balance leastconn
cookie WSID insert indirect nocache httponly samesite strict
option http-server-close
timeout tunnel 3600s
server ws-01 10.10.10.11:8080 check cookie ws01
server ws-02 10.10.10.12:8080 check cookie ws02
server ws-03 10.10.10.13:8080 check cookie ws03
The
insertkeyword tells HAProxy to inject the cookie on behalf of the backend, so no application changes are required. The
indirectkeyword strips the cookie before forwarding the request to the backend, keeping backend logs clean. The
nocachekeyword prevents any intermediate proxy from caching the response that carries the Set-Cookie header. The
httponlyand
samesite strictflags are security hardening: they prevent JavaScript from reading the cookie and restrict cross-site submission respectively.
Each
serverline includes a
cookievalue. HAProxy uses this value as the cookie payload. When it sees a request with
Cookie: WSID=ws02, it routes that request directly to the
ws-02server, bypassing the load balancing algorithm entirely. If the target server is down, HAProxy will route to an available server and update the cookie value accordingly.
For clients that do not support cookies — native mobile applications, IoT devices, or programmatic WebSocket clients — source IP hashing provides a simpler fallback. Use consistent hashing to minimize disruption when servers are added or removed:
backend ws_backends_nocookie
balance source
hash-type consistent
option http-server-close
timeout tunnel 3600s
server ws-01 10.10.10.11:8080 check
server ws-02 10.10.10.12:8080 check
server ws-03 10.10.10.13:8080 check
Source IP hashing is less reliable for WebSocket stickiness on mobile networks where clients frequently change public IP addresses. For these scenarios, the preferred architecture is application-level session reconstruction using a shared state store such as Redis or Valkey, allowing any backend to serve any client without routing constraints.
Routing WebSocket and HTTP Traffic with ACLs
Most production deployments serve both standard HTTP and WebSocket traffic on the same hostname and port. Separating them into dedicated backend pools allows independent scaling, different health check configurations, and tuned connection limits. HAProxy ACLs inspect the upgrade headers present only on WebSocket requests and route them accordingly.
frontend https_frontend
bind *:443 ssl crt /etc/haproxy/certs/solvethenetwork.com.pem
bind *:80
http-request redirect scheme https unless { ssl_fc }
# Detect WebSocket upgrade requests by inspecting both required headers
acl is_websocket hdr(Upgrade) -i websocket
acl is_ws_connection hdr_sub(Connection) -i upgrade
# Route WebSocket traffic to the dedicated backend
use_backend ws_backends if is_websocket is_ws_connection
# Standard HTTP traffic
default_backend http_backends
backend ws_backends
balance leastconn
cookie WSID insert indirect nocache httponly samesite strict
option http-server-close
timeout tunnel 3600s
server ws-01 10.10.10.11:8080 check
server ws-02 10.10.10.12:8080 check
server ws-03 10.10.10.13:8080 check
backend http_backends
balance roundrobin
option httpchk
http-check connect
http-check send meth GET uri /healthz ver HTTP/1.1 hdr Host solvethenetwork.com
http-check expect status 200
server app-01 10.10.10.21:8080 check inter 5s fall 3 rise 2
server app-02 10.10.10.22:8080 check inter 5s fall 3 rise 2
server app-03 10.10.10.23:8080 check inter 5s fall 3 rise 2
Requiring both the
Upgrade: websocketheader and the presence of
upgradein the
Connectionheader reduces false positives from non-compliant clients. The
-iflag on both ACLs makes the comparison case-insensitive, which is important because the WebSocket specification does not mandate a specific case. A client sending
upgrade: WebSocketor
connection: UPGRADEwill still be correctly identified and routed.
WebSocket Health Checks
A standard HTTP health check that expects a 200 response will not detect failures in the WebSocket upgrade path. If your backend's WebSocket handler crashes while the HTTP server continues to respond to GET requests, the simple check will report the server as healthy and HAProxy will continue routing WebSocket connections to a broken backend.
The most pragmatic approach for production is to expose a dedicated lightweight endpoint that performs the WebSocket handshake and returns HTTP 101:
backend ws_backends
balance leastconn
cookie WSID insert indirect nocache httponly samesite strict
timeout tunnel 3600s
option httpchk
http-check connect
http-check send meth GET uri /ws/health ver HTTP/1.1 \
hdr Host solvethenetwork.com \
hdr Upgrade websocket \
hdr Connection Upgrade \
hdr Sec-WebSocket-Key dGhlIHNhbXBsZSBub25jZQ== \
hdr Sec-WebSocket-Version 13
http-check expect status 101
server ws-01 10.10.10.11:8080 check inter 10s fall 2 rise 3
server ws-02 10.10.10.12:8080 check inter 10s fall 2 rise 3
server ws-03 10.10.10.13:8080 check inter 10s fall 2 rise 3
HAProxy sends a real WebSocket upgrade request to the
/ws/healthendpoint. The backend performs the handshake and responds with 101. HAProxy verifies the 101 response, then immediately closes the check connection without completing the WebSocket frame exchange. This validates the entire upgrade path. Use
inter 10srather than the default 2s for WebSocket checks because the handshake involves slightly more processing than a plain TCP connect.
If modifying the application to expose a WebSocket health endpoint is not feasible, fall back to an HTTP-level check that at minimum verifies the application process is alive and the port is bound:
backend ws_backends_fallback
balance leastconn
timeout tunnel 3600s
option httpchk
http-check connect
http-check send meth GET uri /healthz ver HTTP/1.1 hdr Host solvethenetwork.com
http-check expect status 200
server ws-01 10.10.10.11:8080 check inter 5s fall 3 rise 2
server ws-02 10.10.10.12:8080 check inter 5s fall 3 rise 2
server ws-03 10.10.10.13:8080 check inter 5s fall 3 rise 2
Preserving Client IP and Forwarding Headers
WebSocket applications commonly need the originating client IP for logging, rate limiting, presence tracking, and geo-routing decisions. HAProxy operates in HTTP mode during the upgrade handshake, so all
http-requestrules execute before the connection enters tunnel mode. Headers injected at this stage are included in the upgrade request forwarded to the backend. Once the 101 response is returned, no further header manipulation occurs.
frontend https_frontend
bind *:443 ssl crt /etc/haproxy/certs/solvethenetwork.com.pem
# Strip X-Forwarded-For from incoming requests to prevent spoofing
http-request del-header X-Forwarded-For
# HAProxy inserts the real client IP
option forwardfor
# Indicate the original protocol to the backend
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Forwarded-Proto http unless { ssl_fc }
acl is_websocket hdr(Upgrade) -i websocket
acl is_ws_connection hdr_sub(Connection) -i upgrade
use_backend ws_backends if is_websocket is_ws_connection
default_backend http_backends
The
option forwardfordirective appends the client's IP to the
X-Forwarded-Forheader before forwarding the upgrade request. If the client sends a forged
X-Forwarded-For, the preceding
http-request del-headerremoves it first, ensuring the backend always sees a trustworthy value. Your WebSocket application should read client IP from this header rather than from the TCP source address, which will always be the HAProxy instance.
Path-Based Routing to Specialized WebSocket Backends
Production applications frequently operate multiple WebSocket endpoints with different characteristics. A chat service holds connections open for hours and requires strict stickiness. A notification feed can tolerate losing a connection because the client will simply reconnect and re-subscribe. A media streaming endpoint holds connections for the full duration of a video stream. Each of these benefits from a separately tuned backend pool.
frontend https_frontend
bind *:443 ssl crt /etc/haproxy/certs/solvethenetwork.com.pem
option forwardfor
http-request del-header X-Forwarded-For
acl is_websocket hdr(Upgrade) -i websocket
acl is_ws_connection hdr_sub(Connection) -i upgrade
acl ws_path_chat path_beg /ws/chat
acl ws_path_notify path_beg /ws/notifications
acl ws_path_stream path_beg /ws/stream
use_backend ws_chat_backends if is_websocket is_ws_connection ws_path_chat
use_backend ws_notify_backends if is_websocket is_ws_connection ws_path_notify
use_backend ws_stream_backends if is_websocket is_ws_connection ws_path_stream
use_backend ws_backends if is_websocket is_ws_connection
default_backend http_backends
backend ws_chat_backends
balance leastconn
cookie WSCHAT insert indirect nocache httponly samesite strict
timeout tunnel 7200s
server chat-01 10.10.10.21:9001 check inter 10s fall 2 rise 3 cookie chat01 maxconn 5000
server chat-02 10.10.10.22:9001 check inter 10s fall 2 rise 3 cookie chat02 maxconn 5000
backend ws_notify_backends
balance roundrobin
timeout tunnel 1800s
server notify-01 10.10.10.31:9002 check inter 5s fall 3 rise 2
server notify-02 10.10.10.32:9002 check inter 5s fall 3 rise 2
backend ws_stream_backends
balance leastconn
timeout tunnel 86400s
server stream-01 10.10.10.41:9003 check inter 10s fall 2 rise 3
server stream-02 10.10.10.42:9003 check inter 10s fall 2 rise 3
Note the different
timeout tunnelvalues per backend. Chat sessions may remain open for hours with long idle gaps between messages. Notification feeds are typically reconnected frequently, so a shorter tunnel timeout reclaims resources from stale connections faster. Streaming sessions may run continuously for a full day. Matching tunnel timeout to the expected connection lifecycle avoids both unnecessary disconnections and resource exhaustion from abandoned connections.
Rate Limiting WebSocket Upgrade Requests
WebSocket upgrade requests are an attractive vector for resource exhaustion. A single malicious or misbehaving client can open thousands of concurrent WebSocket connections, consuming file descriptors, memory, and backend capacity. HAProxy stick tables track connection state per source IP and enforce limits before the upgrade is forwarded to the backend.
frontend https_frontend
bind *:443 ssl crt /etc/haproxy/certs/solvethenetwork.com.pem
# Track concurrent and rate-of-new WebSocket connections per source IP
stick-table type ip size 200k expire 120s store conn_cur,conn_rate(10s)
http-request track-sc0 src if { hdr(Upgrade) -i websocket }
# Block source IPs that hold more than 100 concurrent WebSocket connections
http-request deny deny_status 429 if { hdr(Upgrade) -i websocket } { sc_conn_cur(0) gt 100 }
# Block source IPs attempting more than 30 new WebSocket upgrades per 10 seconds
http-request deny deny_status 429 if { hdr(Upgrade) -i websocket } { sc_conn_rate(0) gt 30 }
acl is_websocket hdr(Upgrade) -i websocket
acl is_ws_connection hdr_sub(Connection) -i upgrade
use_backend ws_backends if is_websocket is_ws_connection
default_backend http_backends
The stick table tracks only WebSocket upgrade requests, not general HTTP traffic. This avoids penalizing heavy REST API users for WebSocket-specific limits. The
conn_curcounter tracks how many open WebSocket connections a source currently holds across all backends. The
conn_rate(10s)counter tracks how many new WebSocket connections the source has initiated in the last 10 seconds. Adjust both thresholds based on your expected legitimate usage patterns — a browser application opening one or two WebSocket connections at login requires very different limits from a server-side aggregator that may legitimately open hundreds.
Complete Production Configuration
The following configuration consolidates all the techniques covered in this article into a single production-ready HAProxy deployment for
solvethenetwork.com. It handles TLS termination, HTTP-to-HTTPS redirection, WebSocket ACL detection, rate limiting, path-based routing, sticky sessions, and differentiated health checks.
global
log /dev/log local0
log /dev/log local1 notice
maxconn 100000
user haproxy
group haproxy
daemon
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
ssl-default-bind-options prefer-client-ciphers ssl-min-ver TLSv1.2 no-tls-tickets
tune.ssl.default-dh-param 2048
tune.maxrewrite 1024
defaults
log global
mode http
option httplog clf
option dontlognull
option forwardfor
option http-server-close
timeout connect 5s
timeout client 30s
timeout server 30s
timeout tunnel 3600s
maxconn 10000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 429 /etc/haproxy/errors/429.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
# -----------------------------------------------
# Frontend: HTTPS + WSS on port 443
# -----------------------------------------------
frontend https_frontend
bind *:443 ssl crt /etc/haproxy/certs/solvethenetwork.com.pem alpn h2,http/1.1
bind *:80
http-request redirect scheme https code 301 unless { ssl_fc }
# Security headers
http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
# Prevent X-Forwarded-For spoofing
http-request del-header X-Forwarded-For
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Forwarded-Proto http unless { ssl_fc }
# WebSocket rate limiting
stick-table type ip size 200k expire 120s store conn_cur,conn_rate(10s)
http-request track-sc0 src if { hdr(Upgrade) -i websocket }
http-request deny deny_status 429 if { hdr(Upgrade) -i websocket } { sc_conn_cur(0) gt 100 }
http-request deny deny_status 429 if { hdr(Upgrade) -i websocket } { sc_conn_rate(0) gt 30 }
# WebSocket detection
acl is_websocket hdr(Upgrade) -i websocket
acl is_ws_connection hdr_sub(Connection) -i upgrade
# Path-based WebSocket routing
acl ws_path_chat path_beg /ws/chat
acl ws_path_notify path_beg /ws/notifications
use_backend ws_chat_backends if is_websocket is_ws_connection ws_path_chat
use_backend ws_notify_backends if is_websocket is_ws_connection ws_path_notify
use_backend ws_backends if is_websocket is_ws_connection
default_backend http_backends
# -----------------------------------------------
# Backend: Chat WebSocket (long-lived, sticky)
# -----------------------------------------------
backend ws_chat_backends
balance leastconn
cookie WSCHAT insert indirect nocache httponly samesite strict
timeout tunnel 7200s
option httpchk
http-check connect
http-check send meth GET uri /ws/health ver HTTP/1.1 hdr Host solvethenetwork.com hdr Upgrade websocket hdr Connection Upgrade hdr Sec-WebSocket-Key dGhlIHNhbXBsZSBub25jZQ== hdr Sec-WebSocket-Version 13
http-check expect status 101
server chat-01 10.10.10.11:9001 check inter 10s fall 2 rise 3 cookie chat01 maxconn 5000
server chat-02 10.10.10.12:9001 check inter 10s fall 2 rise 3 cookie chat02 maxconn 5000
# -----------------------------------------------
# Backend: Notification WebSocket (short-lived)
# -----------------------------------------------
backend ws_notify_backends
balance roundrobin
timeout tunnel 1800s
option httpchk
http-check connect
http-check send meth GET uri /healthz ver HTTP/1.1 hdr Host solvethenetwork.com
http-check expect status 200
server notify-01 10.10.10.21:9002 check inter 5s fall 3 rise 2
server notify-02 10.10.10.22:9002 check inter 5s fall 3 rise 2
# -----------------------------------------------
# Backend: Generic WebSocket
# -----------------------------------------------
backend ws_backends
balance leastconn
cookie WSID insert indirect nocache httponly samesite strict
timeout tunnel 3600s
option httpchk
http-check connect
http-check send meth GET uri /healthz ver HTTP/1.1 hdr Host solvethenetwork.com
http-check expect status 200
server ws-01 10.10.10.31:8080 check inter 10s fall 2 rise 3 cookie ws01
server ws-02 10.10.10.32:8080 check inter 10s fall 2 rise 3 cookie ws02
server ws-03 10.10.10.33:8080 check inter 10s fall 2 rise 3 cookie ws03
# -----------------------------------------------
# Backend: Standard HTTP
# -----------------------------------------------
backend http_backends
balance roundrobin
option httpchk
http-check connect
http-check send meth GET uri /healthz ver HTTP/1.1 hdr Host solvethenetwork.com
http-check expect status 200
server app-01 10.10.10.41:8080 check inter 5s fall 3 rise 2
server app-02 10.10.10.42:8080 check inter 5s fall 3 rise 2
server app-03 10.10.10.43:8080 check inter 5s fall 3 rise 2
Verifying WebSocket Proxying
After deploying the configuration, use
wscatto verify end-to-end WebSocket connectivity from the command line:
# Install wscat
npm install -g wscat
# Test plain WebSocket (should be redirected to wss://)
wscat -c ws://sw-infrarunbook-01.solvethenetwork.com/ws/chat
# Test secure WebSocket
wscat -c wss://sw-infrarunbook-01.solvethenetwork.com/ws/chat
# Verify the upgrade handshake and sticky session cookie with curl
curl -v https://sw-infrarunbook-01.solvethenetwork.com/ws/health \
-H 'Upgrade: websocket' \
-H 'Connection: Upgrade' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
-H 'Sec-WebSocket-Version: 13'
A successful WebSocket check will show a 101 response in the curl output. Look for the
Set-Cookieheader confirming sticky session insertion. Check the HAProxy logs to verify that WebSocket connections are being routed to the correct backend. WebSocket connections will show a 101 status code in the log and a significantly longer duration compared to standard HTTP requests:
# Monitor WebSocket connection establishment in real time
tail -f /var/log/haproxy.log | grep ' 101 '
# Check current backend server state via the Runtime API
echo 'show servers state ws_chat_backends' | socat stdio /var/run/haproxy/admin.sock
# View active connection counts including tunneled connections
echo 'show info' | socat stdio /var/run/haproxy/admin.sock | grep -E 'CurrConns|MaxConn'
Common Pitfalls and Troubleshooting
Immediate disconnection after upgrade: If WebSocket connections drop immediately after the 101 response, verify that the backend is correctly implementing the WebSocket handshake. Use
tcpdumpor Wireshark to capture traffic between HAProxy and the backend on port 8080 (or whichever backend port is in use) to confirm the 101 response is being returned. Also check that
option http-server-closeis not being applied to the wrong backend — this option causes HAProxy to add
Connection: closeto requests, which can interfere with WebSocket upgrades in some edge cases.
Connections dropping after exactly N seconds: This is almost always a timeout misconfiguration. If connections drop after 30 seconds,
timeout clientor
timeout serveris taking effect instead of
timeout tunnel. Verify that
timeout tunnelis set in the defaults block or in the specific backend handling WebSocket connections. Also check whether a firewall or NAT device in the path has its own idle connection timeout that is shorter than HAProxy's tunnel timeout.
Clients reconnecting to different backends: If your application does not handle session reconstruction on reconnect, clients will appear to lose state after a disconnect. Use browser developer tools to verify that the sticky session cookie is being set and sent on subsequent connections. If the cookie is present but HAProxy is still routing to a different backend, check that the cookie name in the frontend matches the backend configuration and that the cookie is not being stripped by application middleware.
WebSocket over HTTP/2: HAProxy supports WebSocket over HTTP/2 via the extended CONNECT method (RFC 8441) starting with version 2.2. If your frontend binds with
alpn h2,http/1.1, some browsers will attempt HTTP/2 WebSocket. Ensure your HAProxy version is 2.2 or higher and that
option http-server-closeis not interfering with HTTP/2 upgrade semantics. You can inspect the negotiated protocol using
ssl_fc_alpnin ACLs if you need to handle HTTP/1.1 and HTTP/2 WebSocket separately.
