QUIC connection migration for real-time apps #
A field technician walks from the warehouse Wi-Fi to the car park, the phone switches to 5G, and the live job board they were watching goes blank for three seconds while the WebSocket dies, reconnects, re-authenticates and resyncs. Over a WebSocket that is the best you can do: a TCP connection is bound to its IP address and port, so a network change always means a new connection. QUIC, the transport under HTTP/3 and WebTransport, was designed to avoid this. Connections are identified by connection IDs rather than addresses, so when the client’s address changes, the same connection can continue on the new path. For real-time apps with mobile users, that turns a visible reconnect into an invisible hiccup — provided your server and load balancers are built to let it happen.
Root cause #
TCP identifies a connection by its four-tuple: source IP, source port, destination IP, destination port. When a phone moves from Wi-Fi to cellular, its source IP changes; to the server, packets from the new address belong to no known connection, and the old connection is orphaned. Even without moving, NAT devices sometimes rebind a mapping to a new external port after idle periods, which has the same effect. The application’s only recourse is to notice, reconnect and resume, as in reconnecting WebSockets after a network change.
QUIC puts a connection ID in every packet header. The server chooses IDs and gives the client several spare ones; the client can start sending from a new address using a fresh ID (to avoid linking its old and new locations for observers), and the server recognizes the connection. Before trusting the new path fully, the server validates it with a challenge/response, which prevents an attacker from redirecting traffic to a victim’s address. Congestion control restarts cautiously on the new path, but encryption keys, streams, and application state all carry over.
Resolution #
Migration works automatically between a supporting browser and server, as long as nothing in between routes by address. Three pieces must line up.
The client needs nothing special: browsers using WebTransport or HTTP/3 migrate on their own when the platform reports a network change (support and behaviour vary by browser and operating system, so treat migration as an improvement, not a guarantee).
The server must support migration and not disable it. Most QUIC stacks support it by default; some deployments turn it off (the disable_active_migration transport parameter) because their load balancing cannot handle it.
The load balancer is where most deployments break it. A UDP load balancer that hashes the four-tuple sends packets from the client’s new address to a different backend, which has never heard of the connection. The fix is to route by connection ID: the backend encodes its identity into the connection IDs it issues, and the balancer decodes it (the approach standardized as QUIC-LB), or you use a managed load balancer that is QUIC-aware.
Keep the application-level resume path regardless. Migration fails when the client’s platform does not trigger it, when the old path dies before the new one is ready, or when the server does not support it, and in those cases the session must still recover the ordinary way:
// Treat migration as a fast path; keep resume as the guaranteed path.
const RESUME_AFTER_MS = 2_000; // if no traffic this long after a network change, verify
export function watchTransport(wt: WebTransport, lastSeq: () => number, reconnect: () => Promise<void>) {
let lastInboundAt = performance.now();
const markInbound = () => { lastInboundAt = performance.now(); };
// Call markInbound() from every stream/datagram reader (omitted for brevity).
(navigator as any).connection?.addEventListener('change', () => {
const changedAt = performance.now();
setTimeout(() => {
// Migration succeeded if data kept flowing after the change.
if (lastInboundAt > changedAt) { metrics.migrationSucceeded.inc(); return; }
metrics.migrationFailed.inc();
void reconnect(); // new session, resume from lastSeq()
}, RESUME_AFTER_MS);
});
wt.closed.catch(() => void reconnect()); // session died outright: resume path
return markInbound;
}
declare const metrics: { migrationSucceeded: { inc(): void }; migrationFailed: { inc(): void } };
Measuring the success rate matters, because it tells you whether the investment is paying off for your users. If most network changes still end in reconnects, look at the load balancer first.
Edge cases #
Privacy and linkability. Using a fresh connection ID on the new path prevents network observers from linking the client’s old and new addresses. Servers should issue enough spare IDs (via NEW_CONNECTION_ID frames) that clients never have to reuse one.
Congestion reset. After migrating, QUIC restarts congestion control on the new path, so throughput briefly drops while it probes the new network’s capacity. Real-time apps with bursty traffic may see a short slowdown instead of an outage — much better, but still worth coalescing updates through, as in coalescing high-frequency WebSocket updates.
Server-side migration. QUIC also lets a server advertise a preferred address after the handshake, which can move clients off an anycast front door onto a specific host. Few deployments use it today, but it is a clean tool for draining and load distribution once widely supported.
Verification #
Test on real devices: open a WebTransport session on a phone connected to Wi-Fi, then disable Wi-Fi so it falls back to cellular. With server logging of connection IDs and client addresses, a successful migration shows the same connection continuing from a new address after a path validation; a failed one shows the old connection timing out and a new session starting with a resume request. Client-side, the success counter from the code above should increase.
# Server-side qlog / debug logging (stack-specific) — look for path validation after a change:
grep -E 'PATH_CHALLENGE|PATH_RESPONSE|migrat' /var/log/quic/server.log | tail -20
For load balancer verification, generate traffic from a test client, change its source port mid-connection (many QUIC test tools can simulate NAT rebinding), and confirm the connection continues on the same backend.
Operational checklist #
FAQ #
Does WebTransport survive switching from Wi-Fi to mobile data? #
It can, through QUIC connection migration, when the browser, operating system, server and load balancers all support it. When any piece is missing, the session drops and the application must reconnect and resume.
Can WebSockets do connection migration? #
No. WebSockets run over TCP, whose connections are tied to IP addresses and ports. A network change always ends the connection; the best a WebSocket app can do is detect it quickly and resume.
What is QUIC-LB? #
A specification for encoding routing information into QUIC connection IDs so load balancers can send every packet of a connection to the same backend even after the client’s address changes. It is what makes migration compatible with horizontally scaled servers.
Is migration a security risk? #
QUIC validates new paths with a challenge/response before sending significant data there, which prevents attackers from redirecting a connection’s traffic to a victim. Connection IDs are also rotated to prevent tracking across networks.
Related #
- WebTransport vs WebSocket — migration as one of WebTransport’s advantages.
- Reconnecting WebSockets After a Network Change — the TCP-world equivalent.
- WebTransport Browser Support and Fallbacks — when migration is not available at all.
- Routing Clients to the Nearest WebSocket Region — edge routing that must stay migration-aware.
Back to WebTransport & HTTP/3.