SSE over HTTP/3 and QUIC Trade-offs Permalink to this section
Part of HTTP/2 and HTTP/3 for Event Streams, under SSE Protocol Fundamentals & Architecture.
HTTP/3 is the first transport change that meaningfully affects long-lived streams on mobile networks, because a QUIC connection can survive the client changing network entirely. Whether that is worth enabling depends on where your users are and what your reconnect telemetry already looks like.
Symptom & Developer Intent Permalink to this section
The signals that make HTTP/3 relevant:
- Mobile users show a reconnect rate several times higher than desktop users on the same feed.
- Reconnect spikes correlate with users moving β leaving a building, boarding a train, switching from wifi to cellular.
- Time-to-first-event on mobile has a long tail dominated by TCP and TLS handshakes rather than by server work.
- A stream drops with no error on either side each time the clientβs IP address changes.
The intent is to decide whether HTTP/3 is worth deploying for streaming traffic and, if so, to enable it without breaking the clients that cannot use it.
Root Cause Analysis Permalink to this section
A TCP connection is identified by the four-tuple of source address, source port, destination address and destination port. Change any of them β which is exactly what happens when a phone hands off from wifi to cellular β and the connection is gone. There is no notification: the old socket simply stops receiving, and neither end learns anything until a timeout expires. For an SSE stream this means a dropped connection and a reconnect, plus the full cost of a new TCP and TLS handshake on the new path.
QUIC identifies a connection by a connection ID carried in the packets themselves, independent of addressing. When the clientβs network changes, packets arrive from a new address with the same connection ID and the server continues the existing connection after a short path validation. The stream is not interrupted, no reconnect occurs, and no replay is needed.
The second difference is head-of-line blocking at the transport layer. Under HTTP/2 all streams share one TCP connection, so a lost packet stalls delivery for every stream on it until retransmission completes β even streams whose data arrived intact. QUIC gives each stream independent delivery, so a loss affecting one stream does not stall the others. For a single SSE stream this is neutral; for a client holding several streams on a lossy network it is a real improvement.
The third is handshake cost. A resumed QUIC connection can carry application data in its first flight, so a reconnect after a genuine drop starts delivering sooner than the TCP-plus-TLS equivalent.
Against this sits one hard constraint: QUIC runs over UDP port 443, and many corporate networks, some mobile carriers and a number of home routers block or throttle UDP. Browsers handle this by falling back to HTTP/2 β but it means HTTP/3 is always an enhancement layered on a working HTTP/2 deployment, never a replacement for one.
Step-by-Step Resolution Permalink to this section
Step 1 β Confirm HTTP/2 is correct first Permalink to this section
HTTP/3 inherits every header rule from HTTP/2, including the ban on hop-by-hop headers. If the stream is not already clean over HTTP/2, fix that first β see Serving SSE over HTTP/2 with nginx.
curl -so /dev/null -w 'h2: %{http_version}\n' https://api.example.com/events
Step 2 β Enable the QUIC listener Permalink to this section
server {
listen 443 ssl; # HTTP/1.1 and HTTP/2
listen 443 quic reuseport; # HTTP/3 over UDP
http2 on;
# Tell clients HTTP/3 is available on this origin.
add_header Alt-Svc 'h3=":443"; ma=86400' always;
# QUIC requires TLS 1.3.
ssl_protocols TLSv1.2 TLSv1.3;
location /events {
proxy_pass http://sse_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 3600s;
}
}
The Alt-Svc header is the whole discovery mechanism: the first request arrives over HTTP/2, the browser notes the advertisement, and subsequent connections attempt HTTP/3 with an automatic fall back to HTTP/2 if UDP does not work.
Step 3 β Open UDP 443 through every layer Permalink to this section
This is where deployments usually stall: the listener is configured and nothing negotiates HTTP/3 because a firewall, security group or load balancer never forwards the UDP packets.
# Host firewall
sudo ufw allow 443/udp
# AWS security group
aws ec2 authorize-security-group-ingress \
--group-id "$SG_ID" --protocol udp --port 443 --cidr 0.0.0.0/0
Network load balancers must be configured to forward UDP as well; classic application load balancers that terminate TCP only will never pass QUIC through.
Step 4 β Verify negotiation end to end Permalink to this section
# Does the origin advertise HTTP/3?
curl -sI https://api.example.com/events | grep -i alt-svc
# alt-svc: h3=":443"; ma=86400
# Does the stream actually negotiate HTTP/3?
curl --http3 -so /dev/null -w '%{http_version}\n' https://api.example.com/events
# 3
Step 5 β Measure connection migration, which is the point of the exercise Permalink to this section
Synthetic tests rarely change network mid-stream, so this needs a real device. Open the stream on a phone connected to wifi, then disable wifi so it falls back to cellular, and watch whether the server sees a new connection.
// Client: log every reconnect with the protocol and the network type, if exposed.
let reconnects = 0;
es.addEventListener('error', () => {
reconnects += 1;
const entry = performance.getEntriesByType('resource').find((r) => r.name.includes('/events'));
beacon('sse_reconnect', {
count: reconnects,
protocol: entry?.nextHopProtocol,
effectiveType: navigator.connection?.effectiveType,
});
});
Over HTTP/2 the wifi-to-cellular handoff produces a reconnect. Over HTTP/3 it should produce none, and the server should log no new connection for that user. That difference is the entire business case.
Validation & Monitoring Permalink to this section
The metric that justifies HTTP/3 is reconnect rate split by protocol, on mobile clients specifically.
# Reconnects per open stream, split by negotiated protocol.
sum(rate(sse_connects_total[15m])) by (http_version)
/ sum(sse_streams_open) by (http_version)
Record http_version as a label on connection metrics (see Observability & Metrics for SSE) and compare the h3 and h2 cohorts over a week of real traffic. A meaningful reduction on mobile is the result to expect; no difference on desktop is also the expected result and is not a failure.
Track HTTP/3 adoption share as well, because it tells you how much of the benefit is reachable at all:
// Share of real sessions that negotiated each protocol.
const entry = performance.getEntriesByType('resource').find((r) => r.name.includes('/events'));
beacon('sse_protocol_share', entry?.nextHopProtocol ?? 'unknown');
Expect the field number to be considerably lower than lab tests suggest. Corporate networks that block UDP and inspect TLS will show http/1.1, not even h2.
Production Checklist Permalink to this section
Frequently Asked Questions Permalink to this section
Does HTTP/3 change anything about the SSE wire format?
Nothing. The event stream format, the field grammar, the blank-line dispatch and Last-Event-ID resume are all identical. Only the transport beneath changes, which is why no client or server application code needs to be touched β the work is entirely in the listener and network configuration.
Is connection migration automatic, or do I have to implement something?
Automatic. It is a QUIC transport feature: packets carry a connection ID that survives an address change, so the server continues the existing connection after validating the new path. Neither your application nor the browser's EventSource is involved, which is exactly why the reconnect that used to happen simply stops happening.
What happens to clients on networks that block UDP?
They use HTTP/2. The browser attempts HTTP/3 based on the Alt-Svc advertisement, fails to establish a QUIC connection, and falls back β usually fast enough to be invisible. This is why HTTP/3 must be an addition to a working HTTP/2 deployment rather than a replacement.
Is it worth enabling for a desktop-only internal application?
Rarely. The benefits that matter for streams β connection migration and per-stream loss isolation β are properties of mobile and lossy networks. On a stable corporate LAN, HTTP/2 already delivers everything a stream needs, and the operational cost of opening UDP through the network is real.