InfraRunBook
    Back to articles

    HAProxy WebSocket Proxying: HTTP Upgrade, Tunnel Mode, Timeouts, and Sticky Sessions

    HAProxy
    Published: Mar 25, 2026
    Updated: Mar 25, 2026

    Learn how to proxy WebSocket and WSS traffic through HAProxy in production, covering HTTP Upgrade detection, tunnel mode timeouts, sticky sessions, ACL-based path routing, and health checks.

    HAProxy WebSocket Proxying: HTTP Upgrade, Tunnel Mode, Timeouts, and Sticky Sessions

    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 by
      timeout 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
      and
      timeout server
      once the 101 response is exchanged. If this directive is absent, HAProxy falls back to
      timeout client
      and
      timeout 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,
    3600s
    is a reasonable starting point. Long-running streaming connections may warrant
    86400s
    or 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

    insert
    keyword tells HAProxy to inject the cookie on behalf of the backend, so no application changes are required. The
    indirect
    keyword strips the cookie before forwarding the request to the backend, keeping backend logs clean. The
    nocache
    keyword prevents any intermediate proxy from caching the response that carries the Set-Cookie header. The
    httponly
    and
    samesite strict
    flags are security hardening: they prevent JavaScript from reading the cookie and restrict cross-site submission respectively.

    Each

    server
    line includes a
    cookie
    value. 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-02
    server, 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: websocket
    header and the presence of
    upgrade
    in the
    Connection
    header reduces false positives from non-compliant clients. The
    -i
    flag 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: WebSocket
    or
    connection: UPGRADE
    will 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/health
    endpoint. 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 10s
    rather 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-request
    rules 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 forwardfor
    directive appends the client's IP to the
    X-Forwarded-For
    header before forwarding the upgrade request. If the client sends a forged
    X-Forwarded-For
    , the preceding
    http-request del-header
    removes 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 tunnel
    values 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_cur
    counter 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

    wscat
    to 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-Cookie
    header 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

    tcpdump
    or 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-close
    is not being applied to the wrong backend — this option causes HAProxy to add
    Connection: close
    to 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 client
    or
    timeout server
    is taking effect instead of
    timeout tunnel
    . Verify that
    timeout tunnel
    is 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-close
    is not interfering with HTTP/2 upgrade semantics. You can inspect the negotiated protocol using
    ssl_fc_alpn
    in ACLs if you need to handle HTTP/1.1 and HTTP/2 WebSocket separately.

    Frequently Asked Questions

    What is the difference between timeout tunnel and timeout client in HAProxy for WebSocket connections?

    timeout client applies to the client-facing side of a connection during normal HTTP operation. timeout tunnel overrides both timeout client and timeout server specifically for connections that have entered tunnel mode, which includes WebSocket connections after the HTTP 101 upgrade. If timeout tunnel is not set, HAProxy uses timeout client and timeout server for tunneled connections, which are typically configured to values like 30s or 60s — far too short for idle WebSocket connections. Always set timeout tunnel explicitly in any backend that handles WebSocket traffic.

    Does HAProxy support WebSocket over HTTP/2 (WSS via HTTP/2)?

    Yes, starting with HAProxy 2.2. WebSocket over HTTP/2 uses the extended CONNECT method defined in RFC 8441, which is distinct from the HTTP/1.1 Upgrade mechanism. HAProxy handles this automatically when the frontend is configured with alpn h2,http/1.1. For deployments still on HAProxy 1.x or 2.0/2.1, WebSocket over HTTP/2 is not supported, and browsers will fall back to HTTP/1.1 for WebSocket connections.

    Why do my WebSocket connections drop after exactly 30 seconds?

    This is almost always caused by timeout client or timeout server being set to 30s in the defaults block without a corresponding timeout tunnel directive. Once HAProxy enters tunnel mode after the 101 response, it still observes these timeouts if timeout tunnel is absent. Add timeout tunnel 3600s (or an appropriate value for your application) to your defaults block or directly in the backend serving WebSocket connections. Also check for external firewall or NAT session timeout settings that may be shorter than HAProxy's configuration.

    Should I use mode tcp or mode http for WebSocket proxying in HAProxy?

    Use mode http. HTTP mode gives you ACL-based routing to inspect the Upgrade header and route WebSocket traffic to dedicated backends, SSL/TLS termination before the upgrade, header injection (X-Forwarded-For, X-Forwarded-Proto), and cookie-based sticky sessions during the handshake phase. TCP mode bypasses all of this and treats the connection as a raw byte stream from the start. The only valid reason to use TCP mode for WebSocket is if you are proxying an already-encrypted WebSocket stream (wss://) without terminating TLS at HAProxy.

    How do I verify that sticky sessions are working for WebSocket connections?

    Use curl with the WebSocket upgrade headers to perform the initial handshake and capture the Set-Cookie header: 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'. Look for the Set-Cookie response header containing your configured cookie name (e.g., WSID=ws01). On subsequent requests, pass this cookie using curl's -b flag and observe in the HAProxy logs that the same backend server is selected.

    How do I configure different tunnel timeouts for different WebSocket backends?

    Set timeout tunnel directly in each backend block rather than (or in addition to) the defaults block. A backend-level timeout tunnel overrides the defaults value for that specific backend. For example, a chat backend might use timeout tunnel 7200s while a notification backend uses timeout tunnel 1800s. This allows you to precisely match connection lifetime expectations per service without affecting other backends in the same HAProxy instance.

    What happens to active WebSocket connections when a backend server is taken out of rotation?

    Existing WebSocket tunnels on a server being removed remain active until the client disconnects or the tunnel timeout expires. HAProxy does not forcibly close existing tunneled connections when a server is marked down via the runtime API or health check failure. New connections will not be routed to the drained server, but in-flight WebSocket sessions continue until they terminate naturally. To gracefully drain WebSocket connections, use the runtime API to set the server to maintenance mode, then wait for the tunnel timeout period before removing the server from the pool.

    Can I limit the number of WebSocket connections per client IP in HAProxy?

    Yes, using stick tables. Configure a stick table on the frontend with the conn_cur counter to track concurrent connections per source IP, and the conn_rate counter to track the rate of new connections. Use http-request track-sc0 src scoped to requests with the Upgrade: websocket header, then add http-request deny rules that check sc_conn_cur(0) and sc_conn_rate(0) against your thresholds. Return HTTP 429 Too Many Requests using deny_status 429 so well-behaved clients can handle the limit gracefully.

    How do I pass client IP information to the WebSocket backend when HAProxy terminates TLS?

    Use option forwardfor in the defaults block or the frontend, combined with http-request del-header X-Forwarded-For to strip any forged values from clients before HAProxy adds its own. Also add http-request set-header X-Forwarded-Proto https if { ssl_fc } so the backend knows the original protocol. These headers are injected during the HTTP upgrade request phase, before the 101 response, and are included in the upgrade request that the backend receives. After the tunnel is established, no further header manipulation occurs.

    What is the correct way to health check a WebSocket backend in HAProxy?

    The most thorough approach is to use http-check send to issue a real WebSocket upgrade request to a dedicated health endpoint, then use http-check expect status 101 to verify that the backend correctly performs the handshake. This catches failures in the WebSocket upgrade path that a plain HTTP 200 check would miss. If modifying the application to expose a WebSocket health endpoint is not practical, fall back to http-check send with a GET request to an HTTP health endpoint and http-check expect status 200. This at least confirms the process is running and the port is bound.

    Related Articles

    HAProxy WebSocket Proxying: HTTP Upgrade,... | InfraRunBook