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.

Extended CONNECT inside an HTTP/2 connection The server advertises the extended CONNECT setting; the browser loads the page on stream one, opens a WebSocket with CONNECT and protocol websocket on stream three, receives 200, and exchanges WebSocket frames as DATA on that stream while other requests continue on other streams. Extended CONNECT inside an HTTP/2 connection Browser Server SETTINGS: ENABLE_CONNECT_PROTOCOL=1 stream 1: GET / (page) stream 3: CONNECT :protocol=websocket stream 3: :status 200 stream 3: DATA = WS frames stream 5: GET /api (still multiplexed) No extra TCP or TLS handshake — the socket is just another stream
RFC 8441 turns a WebSocket into one HTTP/2 stream among many.

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.

RFC 8441 support at a glance Chrome, Edge and Firefox use WebSockets over HTTP/2 when the server advertises support; Safari support is limited; Envoy supports it with allow_connect; nginx and the Node ws library keep WebSockets on HTTP/1.1. RFC 8441 support at a glance Supports H2 WebSockets Notes Chrome, Edge yes when server advertises Firefox yes when server advertises Safari limited falls back to HTTP/1.1 Envoy yes allow_connect nginx not as a server feature WS stays on HTTP/1.1 Node ws library no terminate at the proxy Support changes over time — verify with the checks below rather than trusting a table
The browser and your edge proxy both have to opt in.

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.

Time to an open socket on a 150 ms RTT link A classic WebSocket spends about 375 milliseconds on TCP, TLS and the upgrade request; over an existing HTTP/2 connection only the 75-millisecond CONNECT request remains. Time to an open socket on a 150 ms RTT link when the page already holds an HTTP/2 connection ms until the socket is open 0 ms 100 ms 200 ms 300 ms 400 ms HTTP/1.1 WebSocket 75 ms HTTP/2 extended CONNECT
Reusing the page's connection removes the TCP and TLS handshakes, not the request itself.

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.

Back to Protocol Handshake Mechanics.