<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Http3 · XB Field Notes</title><link>https://www.xbcatm.com/tags/http3/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://www.xbcatm.com/tags/http3/index.xml" rel="self" type="application/rss+xml"/><item><title>HTTP3 rides UDP while HTTP2 stays safe over TCP</title><link>https://www.xbcatm.com/posts/nginx-http3-http2-fallback/</link><pubDate>Sat, 12 Feb 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/nginx-http3-http2-fallback/</guid><description>HTTP/3 exchanges the TCP head of line problem for QUIC over UDP, and HTTP/2 remains the dependable fallback for older clients.</description><content:encoded>&lt;p&gt;HTTP/3 runs QUIC over UDP:443, and the reason to care is what a lost packet costs. On a TCP connection every stream shares one byte stream, so a dropped segment blocks delivery of everything behind it until the retransmission arrives. QUIC gives each stream its own loss recovery, so the same drop blocks only the streams whose data was in that packet.&lt;/p&gt;
&lt;p&gt;The awkward part is that this is not a flag you can turn on: the client must learn the server speaks HTTP/3 before it can try, and the path it learns that over is the one you were trying to improve.&lt;/p&gt;
&lt;h2 id="two-transports-one-port-number"&gt;Two transports, one port number&lt;/h2&gt;
&lt;p&gt;UDP:443 and TCP:443 are separate namespaces, so a single listener can hold both. That is why an nginx server block can carry &lt;code&gt;listen 443 quic reuseport;&lt;/code&gt; next to &lt;code&gt;listen 443 ssl; http2 on;&lt;/code&gt; and serve both on one port number. The &lt;code&gt;reuseport&lt;/code&gt; matters more than it looks: without it, a second bind of UDP:443 fails with EADDRINUSE, which is the error you get when two server blocks both claim the QUIC listener.&lt;/p&gt;
&lt;p&gt;QUIC is not HTTP/2 moved onto UDP. It folds the transport and TLS handshakes into one flight — one round trip, zero on a resumed session — and carries a connection ID independent of address and port. That ID is what makes connection migration possible, and it is also what breaks load balancers that assume a flow stays on one 5-tuple.&lt;/p&gt;
&lt;h2 id="alt-svc-is-how-the-client-finds-out"&gt;Alt-Svc is how the client finds out&lt;/h2&gt;
&lt;p&gt;The first connection to an origin is always over HTTP/2 on TCP, because the client has no idea yet. The response carries an advertisement: &lt;code&gt;Alt-Svc: h3=&amp;quot;:443&amp;quot;; ma=86400&lt;/code&gt;. Parsed, that is protocol &lt;code&gt;h3&lt;/code&gt;, authority &lt;code&gt;:443&lt;/code&gt; and a freshness lifetime of 86400 seconds, or 24 hours. On the next connection to that origin the client tries QUIC first, so the upgrade lands on a subsequent visit and never on the first one.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ma&lt;/code&gt; is the part that bites. While the advertisement is fresh, the client tries HTTP/3 first on every new connection — including after HTTP/3 has failed repeatedly. Model UDP:443 silently dropped by a middlebox: the client burns 15 seconds in QUIC probe-timeout backoff (1, 2, 4 then 8 seconds) before falling back to HTTP/2. At 20 visits a day that is 300 wasted seconds daily until the cache expires a day later. Start the TCP connection in parallel instead of waiting for QUIC to give up, and the identical blocked path resolves in 0.1 seconds. The fallback penalty is a design choice, not a protocol cost.&lt;/p&gt;
&lt;h2 id="0-rtt-and-migration-are-the-two-sharp-edges"&gt;0-RTT and migration are the two sharp edges&lt;/h2&gt;
&lt;p&gt;0-RTT lets a returning client send application data in the very first flight, using a ticket from the previous session. That data is replayable — anyone who captures the flight can send it again — so only requests that are safe to repeat belong in it. A &amp;ldquo;create order&amp;rdquo; call in 0-RTT is a duplicate order waiting for a reason.&lt;/p&gt;
&lt;p&gt;Connection migration is the other edge. Because QUIC keys the connection on its connection ID rather than the address, a client that roams from Wi-Fi to cellular keeps the same connection. Put a load balancer that hashes the 5-tuple in front of that and the continued connection lands on a different backend: with eight backends, a model of the same client before and after roaming picks backend 7 and then backend 4. The protocol&amp;rsquo;s mobility becomes the load balancer&amp;rsquo;s problem.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The first failure is an Alt-Svc cached on the client while the network drops UDP. It presents as a slow first byte on every navigation for a whole day, with nothing unusual in the server log — the QUIC Initials never arrive, so there is nothing to log. Compare server-side HTTP/3 connection counts against client-perceived latency; a wide gap with a healthy h2 path is the signature. Shorten &lt;code&gt;ma&lt;/code&gt;, race the TCP connection, and do not advertise h3 on paths you cannot verify.&lt;/p&gt;
&lt;p&gt;The second failure is connection migration breaking session affinity. A mobile client changes address mid-session, HTTP/3 keeps the connection alive, and the backend that now owns it has none of the cache or session state the old one had. It presents as intermittent auth failures, cache misses and half-completed uploads, only ever on mobile clients, which is what makes it hard to reproduce. Route on connection ID, or pin affinity to something that survives the address change.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Confirm both paths are actually alive:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl -sI https://example.com/ | grep -i alt-svc
curl -s --http3-only -o /dev/null -w &amp;#39;%{http_version} %{time_appconnect}\n&amp;#39; https://example.com/
nginx -T 2&amp;gt;/dev/null | grep -nE &amp;#39;listen .*quic|http3|Alt-Svc&amp;#39;
ss -lun | grep -E &amp;#39;:443\b&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first should print an &lt;code&gt;h3&lt;/code&gt; advertisement with a bounded &lt;code&gt;ma&lt;/code&gt;. The second prints &lt;code&gt;3&lt;/code&gt; and a small connect time if UDP works, or fails outright if the path blocks it — a failure here while the first succeeds is the middlebox case. The third shows whether the config actually wires QUIC and the header together, and the fourth proves a UDP socket is bound on 443.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9114.html"&gt;RFC 9114 HTTP/3&lt;/a&gt;, HTTP/3 semantics over QUIC, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7838.html"&gt;RFC 7838 HTTP Alternative Services&lt;/a&gt;, the Alt-Svc header and its freshness lifetime, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nginx.org/en/docs/http/ngx_http_v3_module.html"&gt;ngx_http_v3_module&lt;/a&gt;, the nginx QUIC and HTTP/3 directives, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9000.html"&gt;RFC 9000 QUIC&lt;/a&gt;, stream-level loss recovery, connection IDs and 0-RTT, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>HTTP/3 and QUIC recover per stream</title><link>https://www.xbcatm.com/posts/http3-and-quic-recover-per-stream/</link><pubDate>Sun, 21 Mar 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/http3-and-quic-recover-per-stream/</guid><description>By running over UDP with its own reliability, QUIC lets one slow stream recover without stalling the others; HTTP/3 is the mapping on top.</description><content:encoded>&lt;p&gt;The next version of HTTP moves from TCP to QUIC, a transport with its own reliability built on UDP. The payoff is that packet loss on one stream no longer stalls the whole connection the way it can on TCP with HTTP/2.&lt;/p&gt;
&lt;h2 id="why-tcp-was-the-ceiling"&gt;Why TCP was the ceiling&lt;/h2&gt;
&lt;p&gt;HTTP/2 multiplexes many streams over one TCP connection, but TCP itself only knows a single ordered byte stream. When a packet is lost, TCP retransmits and requires in-order delivery, so every stream waiting on that connection halts until recovery completes. That is the head-of-line block HTTP/2 inherits from its transport. The fix is not more TCP tuning but a different transport: give each stream its own reliable, independently-ordered delivery, so a loss in one stream does not stall its neighbors. This is the specific ceiling HTTP/3 is built to remove.&lt;/p&gt;
&lt;h2 id="quic-changes-the-transport-not-the-semantics"&gt;QUIC changes the transport, not the semantics&lt;/h2&gt;
&lt;p&gt;HTTP/3 keeps the ideas HTTP/2 introduced, multiplexing, header compression, and stream priority, but runs them over QUIC, which lives on UDP and owns its own reliability. QUIC implements per-stream flow control and loss recovery, carries its own encryption, and can migrate a connection when the network path changes, all features TCP ties to the connection. From an application&amp;rsquo;s point of view the semantics barely change: same methods, headers, and status codes. What moves is the way the bytes are protected, and where connection repair can happen, which is what lets one lossy stream stop dragging the whole page.&lt;/p&gt;
&lt;h2 id="when-it-matters-most"&gt;When it matters most&lt;/h2&gt;
&lt;p&gt;Per-stream recovery pays for itself on lossy paths: poor mobile coverage, congested last-mile links, and lightly packet-lossy networks, exactly the places where a single stalling stream used to hold an entire page hostage. It is also a negotiated upgrade, enabled when both the client and server agree, and falling back to HTTP/2 without any action from the site operator where QUIC is unavailable. Rolling forward is therefore gradual and compatible; the deployment risk is low, and the benefit concentrates exactly where users feel it, on unreliable connections.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;QUIC still rides on UDP, and some networks treat that as suspicious. Enterprise firewalls and older middleboxes may throttle or drop UDP, or pass small DNS-sized datagrams while discarding larger QUIC packets. The symptom is rarely a clean error. Alt-Svc advertises HTTP/3, the client attempts QUIC, and the handshake times out before falling back to HTTP/2 over TCP. If the fallback timer is too generous, users feel the upgrade as a regression, so track handshake success by network type and keep TCP ready.&lt;/p&gt;
&lt;p&gt;0-RTT resumption is a second trap. Replaying the ClientHello saves a round trip, but anything accepted as early data can be replayed by an observer. RFC 9001 therefore limits early data to replay-safe operations. Sending a non-idempotent POST in 0-RTT risks executing the same order twice. Servers must reject risky early data or add anti-replay handling.&lt;/p&gt;
&lt;p&gt;Connection migration has its own edge. QUIC identifies a connection by ID rather than the four-tuple, so a phone moving from Wi-Fi to cellular can continue without a new handshake. A NAT rebinding looks similar, however, and a stale NAT binding or a load balancer that hashes on addresses can send return packets nowhere. Endpoints must validate the new path before trusting it.&lt;/p&gt;
&lt;p&gt;Finally, do not promise that QUIC removes every queue. It removes transport-level head-of-line blocking between streams, but connection-wide congestion control still applies. A saturated bottleneck delays all streams together. Per-stream recovery isolates loss, not overload.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Save the following as &lt;code&gt;quic_streams.py&lt;/code&gt; in a scratch directory and run &lt;code&gt;python3 quic_streams.py&lt;/code&gt;. It models four streams of ten packets each with one packet lost on stream zero, then compares an idealized QUIC scheduler against an idealized TCP scheduler. No sockets are opened; the numbers are a scheduling calculation, not a wire capture.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;STREAMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;PACKETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;LOST_STREAM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;RETRANSMIT_PENALTY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quic_completion&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;Each stream recovers on its own: only the lossy stream pays.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;completions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STREAMS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;LOST_STREAM&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PACKETS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;RETRANSMIT_PENALTY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PACKETS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;completions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tcp_completion&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;One ordered byte stream: a single loss blocks every stream.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;blocked_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PACKETS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;RETRANSMIT_PENALTY&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;blocked_at&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;STREAMS&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;quic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quic_completion&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tcp_completion&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Only the stream that lost a packet waits for retransmission.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;quic&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LOST_STREAM&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PACKETS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;RETRANSMIT_PENALTY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PACKETS&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;LOST_STREAM&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;quic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# TCP cannot release later bytes until the loss is repaired.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PACKETS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;RETRANSMIT_PENALTY&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;tcp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;quic_blocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;quic&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PACKETS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tcp_blocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tcp&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PACKETS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;quic_blocked&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;tcp_blocked&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;STREAMS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tcp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;QUIC completion: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;quic&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (blocked streams: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;quic_blocked&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;TCP completion: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (blocked streams: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tcp_blocked&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;PASS: loss stalls one QUIC stream but all TCP streams&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The QUIC schedule completes the three healthy streams at time ten while only the lossy stream waits until time thirteen. The TCP schedule completes every stream at time thirteen because the single ordered byte stream cannot release later bytes until the loss is repaired. The script asserts that isolation, then prints both vectors. The model ignores congestion and reordering; it isolates loss recovery so the difference is visible in one run.&lt;/p&gt;
&lt;h2 id="further-reading"&gt;Further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/posts/http2-multiplexing-fixes-head-of-line/"&gt;HTTP/2 multiplexing fixes a queue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/posts/tls-1-3-handshake-basics/"&gt;TLS 1.3 handshake basics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cloudflare.com/learning/performance/what-is-http3/"&gt;Cloudflare: HTTP/3 and QUIC&lt;/a&gt;, the protocol rationale, checked 2027-03-19.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9000.html"&gt;RFC 9000: QUIC&lt;/a&gt;, the transport protocol, checked 2027-03-19.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9114.html"&gt;RFC 9114: HTTP/3&lt;/a&gt;, HTTP over QUIC, checked 2027-03-19.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>