The WebSocket handshake over HTTP/2 (RFC 8441) #
Your site is served over HTTP/2, but every WebSocket still opens a separate HTTP/1.1 TCP connection with its own TLS handshake. Load balancers show two kinds of connections per user, the WebSocket ones cannot share the HTTP/2 connection’s congestion window or priority, and on slow mobile networks the extra TLS handshake adds hundreds of milliseconds to time-to-live-data. HTTP/2 does not support the HTTP/1.1 Upgrade mechanism at all, so the classic handshake cannot run on it. RFC 8441 fixes that with an “extended CONNECT” method that opens a WebSocket as one stream inside an existing HTTP/2 connection. Understanding it matters both for the performance win and because it quietly changes how your proxies see WebSocket traffic.
Root cause #
HTTP/2 multiplexes many streams over one connection and explicitly forbids connection-specific headers such as Connection and Upgrade. The HTTP/1.1 WebSocket handshake — a GET with Upgrade: websocket that turns the whole TCP connection into a WebSocket — therefore cannot exist in HTTP/2: there is no way to hand the entire multiplexed connection to one WebSocket.
Before RFC 8441, browsers solved this by opening a dedicated HTTP/1.1 connection for each WebSocket. That works, but it costs a TCP and TLS handshake per socket, bypasses HTTP/2 connection coalescing, and means the WebSocket and the page’s other requests compete as separate connections rather than as prioritized streams.
RFC 8441 adds a setting, SETTINGS_ENABLE_CONNECT_PROTOCOL, that a server advertises, and a pseudo-header, :protocol, used with the CONNECT method. A client that sees the setting sends :method = CONNECT, :protocol = websocket, plus :path and :authority; the server responds 200, and the stream then carries WebSocket frames in both directions. There is no Sec-WebSocket-Key or Sec-WebSocket-Accept; HTTP/2’s own framing already rules out the confusion those headers guard against.
Resolution #
Whether you get RFC 8441 depends on every hop the browser talks to, since the browser only uses it when the endpoint it connects to advertises the setting. In practice that endpoint is your edge proxy, which then speaks ordinary HTTP/1.1 WebSockets to your backend. Enable it at the edge where supported, keep backends on the classic handshake, and verify which one browsers actually used.
# Envoy: accept WebSockets over HTTP/2 from clients, proxy as HTTP/1.1 upgrades upstream.
http_connection_manager:
codec_type: AUTO
http2_protocol_options:
allow_connect: true # advertise SETTINGS_ENABLE_CONNECT_PROTOCOL
upgrade_configs:
- upgrade_type: websocket # H1 Upgrade and H2 extended CONNECT both map here
route_config:
virtual_hosts:
- name: rt
domains: ["rt.example.com"]
routes:
- match: { prefix: "/ws" }
route: { cluster: realtime_h1, timeout: 0s }
# The upstream cluster uses HTTP/1.1, so Envoy performs a classic Upgrade to the backend.
Node.js can also terminate RFC 8441 directly, but the mainstream ws library does not implement the HTTP/2 path, so it is uncommon to serve it from the application itself. For most teams the proxy is the right place: browsers get the connection reuse, while the backend keeps its well-tested HTTP/1.1 WebSocket server. The Envoy details — timeouts and affinity — are in Envoy WebSocket proxy configuration.
Client code does not change at all. new WebSocket('wss://rt.example.com/ws') uses HTTP/2 automatically when an existing HTTP/2 connection to that origin advertises support, and falls back to HTTP/1.1 otherwise.
When it helps, and when it does not #
The benefit is connection reuse: no extra TCP and TLS handshake when the page already has an HTTP/2 connection to the same origin, one congestion window, and fewer connections through every middlebox. That matters most on high-latency mobile networks, where each handshake round trip costs 100 ms or more, and for users who open and close sockets frequently.
The cost is shared fate. All streams on an HTTP/2 connection share one TCP connection, so a lost packet stalls the WebSocket along with every other stream (TCP head-of-line blocking), and a proxy that recycles the HTTP/2 connection — to rotate TLS keys, or on a configuration reload — takes the WebSocket with it. Long-lived WebSocket streams can also keep an HTTP/2 connection open that the proxy would otherwise have closed, which changes connection-lifetime assumptions in load balancers. And monitoring tools that count WebSocket connections by TCP connection or by Upgrade header stop seeing them.
Two operational consequences follow. First, your idle-timeout configuration now includes HTTP/2-level settings (stream idle timeouts, connection max age) as well as WebSocket ones — review them with tuning WebSocket idle timeouts across proxies. Second, if you depend on a separate connection for isolation — for example, a dedicated real-time hostname with different limits — keep the WebSocket on a different origin, which prevents coalescing with the page’s connection.
Verification #
In Chrome DevTools, open the Network panel, add the Protocol column, and load the page. A WebSocket carried by RFC 8441 shows h2 in that column; a classic one shows http/1.1. The chrome://net-export log also records the CONNECT stream on the shared session. From the command line, check that your edge advertises the setting:
# nghttp prints the server SETTINGS frame; look for SETTINGS_ENABLE_CONNECT_PROTOCOL (0x8) = 1.
nghttp -nv https://rt.example.com/ 2>&1 | grep -iE 'SETTINGS|0x08|enable_connect'
Then compare time to first WebSocket message on a throttled “Slow 4G” profile with and without the setting: the HTTP/2 path should save roughly one round trip of TLS handshake time when the page’s connection already exists.
Operational checklist #
FAQ #
Do WebSockets work over HTTP/2? #
Yes, through RFC 8441 extended CONNECT, when both the browser and the server (usually the edge proxy) support it. Otherwise browsers open a separate HTTP/1.1 connection for the WebSocket, which also works fine.
Do I need to change my client code? #
No. The browser chooses the transport automatically. The WebSocket API and frame handling are identical on both.
Is WebSocket over HTTP/2 faster? #
It saves connection setup when a connection to the origin already exists, which helps on high-latency networks. Throughput after setup is similar, and shared TCP head-of-line blocking can make tail latency slightly worse under packet loss.
What about HTTP/3? #
RFC 9220 defines the same extended CONNECT mechanism for HTTP/3, carrying WebSockets over QUIC streams. Support is still emerging; for new designs that want QUIC’s benefits, WebTransport is the more capable option.
Related #
- Sec-WebSocket-Key and Sec-WebSocket-Accept Explained — the HTTP/1.1 handshake this replaces.
- Envoy WebSocket Proxy Configuration — enabling extended CONNECT at the edge.
- HTTP/2 Server Push vs WebSocket — the other HTTP/2 feature often confused with real-time push.
- QUIC Connection Migration for Real-Time Apps — what HTTP/3 adds beyond reuse.
Back to Protocol Handshake Mechanics.