HTTP/3 Stream Prioritization: Optimizing CDN Resource Delivery Order
When a browser loads a modern web page, it requests dozens of resources simultaneously: HTML, CSS, JavaScript, images, fonts, and API responses. The order these resources arrive directly impacts how fast the page renders. Serve a large hero image before the CSS file, and the page renders unstyled for hundreds of milliseconds. Serve a deferred analytics script before the render-blocking stylesheet, and Largest Contentful Paint suffers. HTTP/3's Extensible Priorities scheme (RFC 9218) gives clients a clean mechanism to signal exactly how resources should be prioritized — and gives CDN edge servers the information they need to schedule delivery for optimal rendering performance. This article covers how the priority model works, how CDN edge schedulers should implement it, and the performance impact of getting prioritization right.
Why HTTP/2 Prioritization Failed
HTTP/2 introduced stream prioritization through a dependency tree model. Each stream could declare a parent stream and a relative weight (1-256), creating a tree that the server was supposed to traverse bottom-up to determine scheduling order. The design was elegant in theory but catastrophic in practice.
Browsers implemented radically different tree structures. Chrome created a flat list with weighted round-robin. Firefox built deep dependency chains with exclusive flags. Safari used yet another approach. Each browser expected servers to traverse their specific tree topology to produce the intended scheduling behavior. A server optimized for Chrome's flat structure would produce wrong results for Firefox's deep chains, and vice versa.
The result: most HTTP/2 server implementations (including major CDN platforms) simply ignored priorities entirely, serving streams in whatever order was convenient. Studies showed that fewer than 10% of HTTP/2 servers implemented prioritization correctly. The complex dependency tree was actively harmful — it imposed implementation burden on servers while delivering no benefit because most servers gave up.
The Extensible Priorities Model (RFC 9218)
HTTP/3 adopted the Extensible Priorities scheme, which replaces the dependency tree with two simple parameters per request:
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
- Urgency (u): An integer from 0 (most urgent) to 7 (least urgent), default 3
- Incremental (i): A boolean, default false
The client communicates these via the Priority request header field or via HTTP/3's PRIORITY_UPDATE frame (which allows changing priority after the request is sent).
# Priority header examples
Priority: u=0 # HTML document (highest urgency)
Priority: u=2 # Render-blocking CSS/JS
Priority: u=3, i # Visible images (incremental)
Priority: u=4 # Async scripts
Priority: u=5, i # Below-fold images (incremental)
Priority: u=6 # Prefetched resources
Priority: u=7 # Speculative preloads
# PRIORITY_UPDATE frame (reprioritize after discovery)
# Client discovers a lazy-loaded image is now in viewport
PRIORITY_UPDATE {
Stream ID: 42,
Priority: u=3, i
}
The brilliance of this model is its simplicity. A correct server implementation needs only two rules: (1) serve higher-urgency streams before lower-urgency ones, and (2) within the same urgency level, interleave incremental streams but serialize non-incremental ones. No tree traversal, no weight arithmetic, no cross-browser compatibility matrix.
CDN Edge Scheduler Implementation
A CDN edge server's stream scheduler is the component that decides which QUIC stream gets to send data in any given scheduling round. With HTTP/3 Extensible Priorities, the scheduler design is straightforward but has important nuances at scale.
The Basic Algorithm
class H3PriorityScheduler:
def __init__(self):
# 8 urgency levels, each with two queues
self.levels = [
{"non_incremental": deque(), "incremental": []}
for _ in range(8)
]
def add_stream(self, stream):
level = self.levels[stream.urgency]
if stream.incremental:
level["incremental"].append(stream)
else:
level["non_incremental"].append(stream)
def next_chunk(self, max_bytes):
for urgency in range(8): # 0 = highest
level = self.levels[urgency]
# Non-incremental: serve one stream until complete
if level["non_incremental"]:
stream = level["non_incremental"][0]
chunk = stream.read(max_bytes)
if stream.is_complete():
level["non_incremental"].popleft()
return chunk
# Incremental: round-robin across all streams
if level["incremental"]:
streams = level["incremental"]
chunk_per = max_bytes // len(streams)
chunks = []
for stream in streams[:]:
c = stream.read(chunk_per)
chunks.append(c)
if stream.is_complete():
streams.remove(stream)
return b"".join(chunks)
return None # No data to send
Handling Priority Updates
HTTP/3's PRIORITY_UPDATE frame allows clients to reprioritize streams after they are opened. The most common use case: a browser discovers that a lazy-loaded image is now scrolling into the viewport and upgrades it from u=5 (below-fold) to u=3 (visible). The CDN edge must handle this efficiently without disrupting other streams.
def handle_priority_update(self, stream_id, new_urgency, new_incremental):
stream = self.active_streams[stream_id]
old_level = self.levels[stream.urgency]
# Remove from old position
if stream.incremental:
old_level["incremental"].remove(stream)
else:
old_level["non_incremental"].remove(stream)
# Update stream priority
stream.urgency = new_urgency
stream.incremental = new_incremental
# Insert at new position
new_level = self.levels[new_urgency]
if new_incremental:
new_level["incremental"].append(stream)
else:
new_level["non_incremental"].append(stream)
CDN-Specific Prioritization Strategies
While the basic scheduling algorithm follows client signals faithfully, CDN edge servers have information that clients do not. Smart CDNs use this information to make better scheduling decisions.
Cache-Aware Priority Boosting
When the CDN edge server knows that a resource is a cache hit (available locally, zero origin latency), it can boost that resource's effective priority. The reasoning: a cached resource can be served immediately from memory or SSD, while an origin-fetched resource will take at minimum 10-50ms to arrive. Serving cache hits first maximizes the information delivered to the client in the earliest possible time window.
def effective_urgency(self, stream):
base = stream.urgency
if self.is_cache_hit(stream.request):
# Boost cache hits by 1 urgency level (lower = higher priority)
return max(0, base - 1)
if self.is_origin_pending(stream.request):
# Don't penalize, but don't boost either
return base
return base
Server Push Priority Assignment
When the CDN implements HTTP/3 server push, it creates streams without client priority signals. The CDN must assign appropriate priorities based on resource type. Critical CSS and fonts should push at u=1 (just below the HTML document). Scripts push at u=2-4 depending on their loading strategy. Images should rarely be pushed because the client is better positioned to prioritize images based on viewport position.
Cross-Connection Fairness
A CDN edge server handles thousands of concurrent connections. While per-connection prioritization follows client signals, the edge must also enforce fairness across connections. A single connection requesting hundreds of resources should not starve other connections. Most CDN implementations use a two-level scheduler: the outer scheduler allocates bandwidth across connections (weighted fair queuing), and the inner scheduler prioritizes streams within each connection using the HTTP/3 priority model.
Performance Impact of Correct Prioritization
Correct stream prioritization has a measurable impact on Core Web Vitals. Research from web performance teams shows the following improvements when switching from no prioritization (FIFO) to correct HTTP/3 Extensible Priorities:
- Largest Contentful Paint (LCP): 15-25% improvement. LCP benefits from prioritizing render-blocking CSS and the LCP image (typically the hero image) over deferred scripts and below-fold content
- First Contentful Paint (FCP): 10-20% improvement. FCP depends on the HTML document and critical CSS arriving quickly, which u=0 and u=2 ensure
- Cumulative Layout Shift (CLS): 5-10% improvement. Correct prioritization of fonts (u=1) reduces FOIT (Flash of Invisible Text) and subsequent layout shifts when the font arrives
- Time to Interactive (TTI): 8-15% improvement. Prioritizing render-blocking scripts (u=2) over deferred resources reduces the gap between visual readiness and interactivity
These improvements compound on slower connections. On a 3G mobile connection where bandwidth is scarce, the order resources arrive matters far more than on a fast fiber connection that can deliver everything nearly simultaneously. CDN edge servers implementing correct prioritization provide the largest benefit to the users who need it most.
Debugging Priority Issues
When page load performance does not match expectations, priority misconfigurations are a common culprit. Use these diagnostic approaches to identify and fix issues.
# Check priority headers sent by the client
# Using curl with HTTP/3 support
curl --http3 -v https://cdn.example.com/page.html 2>&1 | grep -i priority
# Using Chrome DevTools
# Network panel → right-click columns → enable "Priority"
# Shows both initial priority and any reprioritization
# Server-side priority logging (nginx-quic)
log_format h3_priority '$remote_addr $request_uri '
'$http3_priority $sent_http_priority';
# QUIC connection analysis with qlog
# Enable qlog in your QUIC stack for detailed priority event traces
# qlog records PRIORITY_UPDATE frames, scheduling decisions,
# and per-stream byte counts per scheduling round
Common priority problems at the CDN edge include: servers ignoring priority signals entirely (treating all streams as equal), servers implementing only urgency but not the incremental flag (interleaving non-incremental streams or serializing incremental ones), and priority inversion when cache misses cause high-priority streams to wait for origin while lower-priority cache hits are ready to send.
HTTP/3 Extensible Priorities represents a reset in how browsers and CDN edge servers communicate about resource importance. The simplicity of the urgency-incremental model means CDN implementers can build correct schedulers with modest engineering effort, and the performance benefits of correct prioritization directly improve the Core Web Vitals scores that determine search ranking and user experience. For CDN platforms, getting prioritization right is one of the highest-leverage optimizations available — it costs nothing in compute or bandwidth, only correct implementation of a simple scheduling algorithm applied to every response leaving the edge.