awesome-everything RU
↑ Back to the climb

Networking & Protocols

Alternate paths: QUIC 0-RTT, WebSocket upgrade, connection migration

Crux When the standard HTTP/1.1 request-response path is too slow or too static, two alternate transports take over: HTTP/3 over QUIC (0-RTT, HoL-free streams, mobile migration) and WebSocket upgrade (persistent full-duplex). Both have specific failure modes you must handle.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 14 min

A mobile user on a train reports the site reloads every time the train goes through a tunnel. Meanwhile your WebSocket-based real-time dashboard drops messages silently when one client is slow to consume. Both problems have the same root cause: the transport was designed for stable connections, but reality is not stable. HTTP/3 and WebSocket handle this differently — and both fail in ways you must explicitly design for.

Path A: HTTP/3 over QUIC

Why QUIC instead of TCP. TCP multiplexes streams over one connection, but a single lost packet stalls the entire TCP receive window until retransmission. HTTP/2 over TCP has exactly this problem: one slow stream blocks all other streams on the same connection. QUIC implements streams as independent entities — a lost packet on stream A does not block streams B, C, or D. This stream independence is the primary win on lossy networks (mobile, intercontinental links).

Merged handshake. TCP requires a separate 3-way handshake (1 RTT) before TLS can begin (another 1 RTT minimum). QUIC merges them: the QUIC Initial packet carries the TLS ClientHello. Server responds in one packet with both QUIC transport parameters and TLS ServerHello. Total: 1 RTT for a new connection instead of 2.

0-RTT resumption. For warm connections (user returning to the same origin within the session ticket’s lifetime), the client sends application data inside the QUIC Initial packet — before the server has responded. Effective RTT for first byte: 0 additional round-trips. Constraint: 0-RTT is safe only for idempotent methods (GET, HEAD). POST/PUT requests must wait for the handshake to complete (server rejects 0-RTT for non-idempotent requests with 425 Too Early).

0-RTT max early data. Servers cap how many bytes of early data they accept (typical: 16–64 KB). If the HTTP request + headers exceed this, the rest is sent after the handshake. This cap prevents attackers from using 0-RTT to inject large payloads before authentication completes.

FeatureHTTP/2 + TCPHTTP/3 + QUIC
New connection cost2 RTT (TCP + TLS)1 RTT (merged)
Warm connection (resumption)1 RTT (TLS PSK)0 RTT (0-RTT early data)
Packet loss impactStalls all streams (HoL block)Affects only the lost stream
IP changeConnection brokenMigrates via Connection ID
Congestion controlKernel (not tunable per-flow)User-space (tunable, BBR2, PCC)
Middlebox compatibilityExcellentSome firewalls block UDP

Connection migration (mobile networks)

When Bea’s phone switches from WiFi to cellular, her IP address changes. Under TCP+HTTP/2, the TCP 4-tuple (src IP, src port, dst IP, dst port) is part of the connection identity — an IP change breaks the connection. Bea must re-establish TCP + TLS + HTTP, paying full handshake cost.

Under QUIC, connections are identified by a Destination Connection ID — an opaque token Sven issued at handshake, chosen by Sven, carried in every QUIC packet header. Bea’s kernel sends a PATH_CHALLENGE frame from the new IP. Sven responds with PATH_RESPONSE using the same Connection ID, proving the IP change is legitimate (not an attacker spoofing Bea’s original IP). All in-flight streams continue on the new path. Migration costs one extra round-trip (~50 ms on a good network), but avoids re-establishing the entire connection.

Edge cases

QUIC connection migration only works if the server’s load balancer routes packets with the same Connection ID to the same backend process. This is a deployment constraint: stateless load balancers that route by source IP will break QUIC migration. Solution: route QUIC by Connection ID (Nginx QUIC, Caddy, Cloudflare support this natively).

Path B: WebSocket upgrade for real-time communication

HTTP is request-response — one request, one response. For real-time bidirectional communication (chat, collaborative editing, live dashboards), the client needs to receive messages from the server without issuing a new request for each. WebSocket solves this by upgrading an HTTP connection to a persistent full-duplex channel.

Upgrade handshake. The client sends:

GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <base64 random 16 bytes>

The server responds:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <HMAC of key + magic string>

After 101, the TCP connection is repurposed as a WebSocket. Both sides exchange frames asynchronously — no request-response ordering, no headers on each message.

Frame structure. Each WebSocket frame carries:

  • FIN bit (1 = final fragment, 0 = more fragments follow)
  • Opcode (1=text, 2=binary, 8=close, 9=ping, 10=pong)
  • Masking (client→server mandatory, XOR with 4-byte masking key — cache poisoning defence)
  • Payload length + payload

Large messages are fragmented across multiple frames (FIN=0 for fragments, FIN=1 for final fragment). The receiver buffers until FIN=1 before delivering to the application.

Ping/pong heartbeats. WebSocket does not have a built-in keepalive at the TCP level. After ~75 s without a frame, many network devices time out idle TCP connections. Solution: send ping frames every 15–30 s. The receiver replies pong. If no pong is received within the timeout, the connection is dead and should be reestablished.

Backpressure failure mode. If Bea’s application is slow to consume messages (e.g., heavy JavaScript processing per message), Sven’s send buffer fills. When Sven calls write() on the socket, the kernel returns WOULDBLOCK. Naive server code that spins on WOULDBLOCK spikes CPU; naive code that drops messages loses data. Correct pattern: Sven stops reading from Bea (pauses the inbound stream) until his outbound buffer drains. Proper WebSocket libraries implement this via async/await and stream backpressure APIs.

Trace it
1/5

Trace QUIC + HTTP/3 handshake and compare to TCP + HTTP/2.

1
Step 1 of 5
New connection, HTTP/3. What does the client send first?
2
Locked
Server responds. What is in the server's first packet?
3
Locked
Client has keys. Can it send HTTP request now?
4
Locked
Packet loss on stream 3 (CSS file). What happens to streams 1 and 2 (HTML + JS)?
5
Locked
User's phone switches from WiFi to LTE mid-download. What happens?
Trace it
1/5

Trace WebSocket backpressure failure and the correct fix.

1
Step 1 of 5
Sven broadcasts 1,000 messages/s to 5,000 connected WebSocket clients. One client (Bea) is slow — processing each message takes 10 ms. What happens?
2
Locked
Naive server: spin loop on WOULDBLOCK. What happens?
3
Locked
Second naive approach: drop messages when buffer full. What happens?
4
Locked
Correct approach: backpressure propagation.
5
Locked
If Bea never catches up (offline, slow forever). What is the timeout strategy?
Quiz

Stream independence is QUIC's main win over HTTP/2+TCP. What specific failure does it prevent?

Quiz

Why is client→server WebSocket frame masking mandatory but server→client masking is not?

Recall before you leave
  1. 01
    QUIC connection migration: what is the Destination Connection ID and why does it survive an IP change?
  2. 02
    What happens if a load balancer routes QUIC packets by source IP instead of Connection ID during migration?
  3. 03
    Explain how WebSocket backpressure propagates from application layer down to TCP flow control.
Recap

HTTP/3 over QUIC solves two TCP limitations: head-of-line blocking (a lost packet no longer stalls all streams) and IP-change fragility (QUIC connection IDs survive WiFi-to-cellular handoffs via PATH_CHALLENGE/RESPONSE). The merged QUIC + TLS handshake costs 1 RTT for new connections and 0 RTT for warm resumptions, but 0-RTT is unsafe for non-idempotent requests due to replay risk. WebSocket upgrades an HTTP connection to a persistent full-duplex channel using opcode frames and masking; its main failure mode is backpressure collapse when a slow consumer fills the send buffer — the correct fix is propagating backpressure from the application layer all the way to TCP flow control, not dropping messages or spinning on WOULDBLOCK.

Connected lessons
appears again in258
Continue the climb ↑Observability: distributed traces, USE/RED, and sampling
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.