HTTP/3 maps HTTP semantics to QUIC, utilizing the reliable streams, flow control, congestion control, and TLS 1.3 handshake provided by QUIC. It reduces the impact of transmission gaps in one stream on the ordered delivery of other streams, but does not eliminate all cross-request dependencies.
Methods, status codes, caching, and other aspects inherit general HTTP semantics; the 9-byte frame headers, stream window updates, and priority trees of HTTP/2 are not carried over verbatim. Troubleshooting should distinguish between the HTTP layer, QUIC streams, QUIC packets, and UDP datagrams.
Issue
Key Boundary
Does UDP packet loss mean HTTP content is lost?
QUIC is responsible for reliable stream data recovery
Are streams completely independent of each other?
Ordered delivery is independent, but congestion, quotas, and upper-layer dependencies are still shared
What if QPACK table updates haven't arrived?
Field blocks referencing unreceived table entries may cause blocking
Where is the Stream ID in HTTP/3 frames?
The association is provided by the outer QUIC stream; it is not redundantly encoded in the HTTP frame header
Must the first access go through HTTP/2?
Endpoints can also be discovered via DNS HTTPS records, etc.
Does switching networks or using 0-RTT guarantee faster speeds?
It depends on the path, state, policy, and actual requests
Stream Mapping: Division of Labor for Requests, Control, and Compression Instructions
Requests use client-initiated bidirectional QUIC streams with IDs 0, 4, 8, etc.; requests and responses occupy the two directions of that stream. Unlike HTTP/2, QUIC stream 0 can carry application requests; it is not a dedicated stream for connection control frames.
Unidirectional streams start with the stream type, followed by the content specified for that type. The control stream for each endpoint begins with SETTINGS as the first frame, and SETTINGS cannot be sent again on the same control stream; there is no SETTINGS ACK as in HTTP/2. QPACK encoder and decoder streams carry their respective instruction formats, not table updates wrapped in HTTP HEADERS or DATA frames.
Both parties must allow the peer to establish at least three unidirectional streams, leaving space and flow control budget for control and QPACK. If critical established streams are unexpectedly closed, it results in a connection error; do not treat this as the natural closure after completing a regular request. RFC 9114: Stream Mapping
Frame Headers are Variable-Length Integers, No Longer Fixed at 9 Bytes
HTTP/3 frames consist of two QUIC variable-length integers (Type, Length) and the payload. The length describes only the payload; the stream ID is provided by the outer QUIC stream. General frame integers can use encoding lengths allowed by the protocol; this example selects the shortest encoding.
Value Range
Encoded Bytes
Top Two Bits
0 to 63
1
00
64 to 16,383
2
01
16,384 to 1,073,741,823
4
10
Up to 2^62−1
8
11
An HTTP/3 frame with 5 B of DATA is Type (1 B) + Length (1 B) + Content (5 B), totaling 7 B. For 1000 B of DATA, it is 1+2+1000=1003 B; these numbers do not include QUIC STREAM frames, packet protection, or UDP/IP headers, so they cannot be directly used to claim a fixed percentage of end-to-end savings.
defvarint(value):ifnot0<=value<2**62:raiseValueError('out of range')forsize,prefixin[(1,0),(2,1),(4,2),(8,3)]:ifvalue<2**(8*size-2):return(value|(prefix<<(8*size-2))).to_bytes(size,'big')defdecode(data):size=1<<(data[0]>>6)iflen(data)<size:raiseValueError('truncated')returnint.from_bytes(data[:size],'big')&((1<<(8*size-2))-1), sizefornin[0,63,64,16383,16384,2**30-1,2**30,2**62-1]:value, used=decode(varint(n))assertvalue==nandused==len(varint(n))assertvarint(64).hex()=='4040'small=varint(0)+varint(5)+b'hello'large=varint(0)+varint(1000)+b'x'*1000assertsmall.hex()=='000568656c6c6f'andlen(small)==7assertlen(large)==1003try:decode(bytes([0x40]))exceptValueError:passelse:raiseAssertionError('accepted truncated integer')print('HTTP/3 DATA complete frame bytes:',len(small),len(large))
Requests or responses use HEADERS, optional DATA, and trailer HEADERS; the end of the direction is expressed by the QUIC stream ending, with no END_STREAM flag as in HTTP/2. HEADERS frames can span multiple QUIC STREAM frames or packets and do not require CONTINUATION; packet boundaries are not field block boundaries.
No TCP Head-of-Line Blocking, but Still Face Shared Resources
Assume stream 0 is missing some content, while stream 4 has received all its data; QUIC can continue to deliver stream 4 without waiting for the gap in stream 0. However, the same lost packet may contain data for multiple streams, and congestion window adjustments caused by packet loss may affect the entire connection's transmission.
Connection-level MAX_DATA, per-stream MAX_STREAM_DATA, the number of openable streams, CPU, and server-side queuing may also limit progress. HTTP/3 delegates flow control to QUIC and no longer sends HTTP/2 WINDOW_UPDATE; unlike HTTP/2 which only charges for DATA payload, QUIC stream quotas cover the bytes of that stream, including HTTP frame headers and HEADERS data.
Therefore, "a specific request is not forced to wait due to byte gaps in another stream" is a concrete guarantee; "requests are completely independent of each other" is an overextension. QUIC Streams and Flow Control
QPACK: Choosing Between Compression Benefits and Blocking Risks
HPACK relies on the evolution of compression state in an ordered connection; since HTTP/3 streams can be delivered independently, QPACK separates table updates and acknowledgments into dedicated streams. Field blocks may still reference dynamic items that have not yet been received, expressing decoding dependencies via Required Insert Count.
In the diagram, the decoder has cumulatively inserted only 5 items, while Request A requires 7, so it must wait for updates; Request B uses only static items or literals, with Required Insert Count=0, so it can be decoded. This is the restored insert count; online fields also involve specified count encoding and Base/relative indexing, so simple integer comparison should not be treated as a complete decoder.
The QPACK static table has 99 entries, and its indexing is not compatible with HPACK. The dynamic table is still maintained separately for the encoding direction of each connection; the decoder returns information such as Section Acknowledgment, Stream Cancellation, and Insert Count Increment to help the encoder track dependencies and releasable states. The units and release conditions for these instructions differ, so they cannot be collectively referred to as a single "table synchronization."
SETTINGS_QPACK_MAX_TABLE_CAPACITY and SETTINGS_QPACK_BLOCKED_STREAMS default to 0. The allowed number of blocked streams is not "the number of all currently requested streams"; the encoder can optionally choose known-safe references or literals to avoid increasing potential blocking at the cost of lower compression ratios. Never Indexed does not mean encrypted fields. RFC 9204: QPACK
Why can't fields be decoded even when stream bytes are complete?
Deliver all HEADERS for A first; it still waits for the 6th and 7th insertions; deliver B, and it can be decoded immediately. When the count reaches 6, A still does not meet the dependency; it unlocks only when it reaches 7. This waiting occurs due to QPACK dependencies, not due to byte gaps in A's stream.
Preparing the visual
Why can't fields be decoded even when stream bytes are complete?
Reliable stream delivery completion does not equal field decoding completion. Only blocks referencing dynamic items wait for corresponding insertions; static/literal blocks can complete first.
{"id":"qpack-required-inserts","title":"Why can't fields be decoded even when stream bytes are complete?","summary":"Reliable stream delivery completion does not equal field decoding completion. Only blocks referencing dynamic items wait for corresponding insertions; static/literal blocks can complete first.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"a\" data-i18n=\"a\"></button><button id=\"b\" data-i18n=\"b\"></button><button id=\"insert\" data-i18n=\"insert\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"states\"></h3><div id=\"states\"></div></section><div class=\"metrics\"><div><span data-i18n=\"count\"></span><strong id=\"count\"></strong></div><div><span data-i18n=\"blocked\"></span><strong id=\"blocked\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#states .tr{grid-template-columns:.5fr .8fr 1.7fr}.tr>*{min-height:4.8em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let count=5,got={A:false,B:false};function draw(){const rows=[['stream','RIC','state']];for(const [id,ric] of [['A',7],['B',0]])rows.push([id,ric,!got[id]?'not received':count<ric?'BLOCKED':'decoded']);table('states',rows);set('count',count);set('blocked',Number(got.A&&count<7));$('#insert').disabled=count>=7;$('#states').dataset.snapshot=JSON.stringify({count,got});}$('#a').onclick=()=>{got.A=true;draw()};$('#b').onclick=()=>{got.B=true;draw()};$('#insert').onclick=()=>{count++;draw()};$('#reset').onclick=()=>{count=5;got={A:false,B:false};draw()};draw();","audio":false,"strings":{"a":"Deliver A's HEADERS","b":"Deliver B's HEADERS","blocked":"Number of Streams Waiting for Insertion","count":"Received Insert Count","heading":"Why can't fields be decoded even when stream bytes are complete?","insert":"Deliver one insertion instruction","intro":"Independently deliver field blocks for two requests and encoder insertion instructions, checking dependencies for each stream.","reset":"Reset","scope":"Decoder initial insert count is 5; A's restored Required Insert Count is 7, B is 0; allows 1 blocked stream; does not simulate online encoding of counts, Base, eviction, and encoder ACK state.","scopeLabel":"Model Scope and Conventions","states":"Stream States"}}
Endpoint Discovery: Alt-Svc and DNS HTTPS
Alt-Svc: h3=":443"; ma=86400 indicates that the original service provides an HTTP/3 alternative service on the same host's port 443; max-age describes the cache time for the alternative service information. It is not the cache duration for HTTP content, nor does it require the client to immediately migrate existing connections to it.
Alternative services do not change the original origin. Even if connecting to a different hostname or port, authentication still requires proof of authority to represent the original service; a single location hint cannot be treated as automatic authorization to trust a new identity. RFC 7838: Alternative Services
DNS HTTPS/SVCB records can also provide service parameters such as alpn and port, helping clients discover h3 before access, without needing to receive Alt-Svc via h1/h2 every time. Records are merely discovery materials; client capabilities, certificate validation, and connection results must still meet requirements. RFC 9460
When UDP is blocked, versions are incompatible, or the path is unreachable, the client may race or fall back to HTTP over TCP. The specific wait duration is determined by the implementation, and it cannot be promised that "attempting QUIC failure causes absolutely no delay." Metrics should be split into initial discovery, connection establishment, fallback, and business processing times.
0-RTT and Migration: Dependence on Prior State and Path Validation
0-RTT requires available resumption state and may be rejected by the server. When sending early requests, appropriate remembered HTTP/3 settings and transport parameters must be used; when rejected, when to resend and whether it can be resent must comply with protocol and business agreements.
Early data carries replay risks. Although GET is nominally safe, it does not prove that the actual application has no side effects; operations such as deductions or order submissions cannot be placed in early data solely because of lower latency. HTTP's Early-Data marker and 425 Too Early can help intermediaries and origin servers coordinate; retries after 425 should not be placed in early data again. RFC 8470
Connection migration can maintain connection identity after address changes, but requires the new path to be available, validated, and connection state to persist, subject to endpoint policies. Switching from Wi-Fi to cellular may experience probing, congestion state adjustments, or failures, and cannot guarantee interruption-free switching every time. Server-side load balancing also needs to route appropriate CIDs to instances holding the connection state. QUIC Migration
Cancellation and GOAWAY: Boundaries Differ from HTTP/2
QUIC's RESET_STREAM, STOP_SENDING, etc., participate in request termination, but "canceling a stream" does not equal business transaction rollback. When there is no response, it is necessary to determine if the request might have already been processed before deciding on business retries.
The request stream IDs in the server-side HTTP/3 GOAWAY represent the "no longer accepted" boundary: requests greater than or equal to it will not be processed, while requests less than it may have already been processed. If the boundary is 8, streams 8 and 12 can be retried on other connections, but streams 0 and 4 cannot be assumed unexecuted based solely on this. HTTP/2's Last-Stream-ID is an inclusive upper bound of potentially processed streams; do not copy it into the same comparison operation.
The boundary in multiple GOAWAYs cannot increase. GOAWAYs issued by the client use push IDs and cannot be directly interpreted as the same client request stream IDs. RFC 9114: Exit
Push, DATAGRAM, and Observability
The base HTTP/3 specification still defines push and corresponding permission controls; whether browser or server products support it should be verified separately; do not directly write that the protocol has deprecated all push. Standard HTTP DATA still travels over reliable streams; QUIC supporting DATAGRAM does not mean all HTTP/3 responses automatically become unreliable transmissions.
Packet capture should first confirm QUIC on UDP, version, and connection establishment, then combine available decryption materials or implementation event tracing to view HTTP frames, QPACK blocking, and flow control; relying solely on UDP port 443 cannot determine request success. Application status codes and QUIC transport closure errors should also be recorded separately to distinguish between business rejections and connection failures.
Further Reading
HTTP/2: Frames, multiplexing, HPACK, and exit boundary comparisons.
UDP and QUIC: Packet recovery, CID, and flow control.
TLS 1.3: Handshake and early data security properties.