HTTP/3 Stream Prioritization: Optimizing CDN Resource Delivery Order

· James Thornton · 10 min read

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:

Performance Comparison: Edge vs Origin Processing
Metric Edge Processing Origin Processing Improvement
Latency (p50)8–15 ms120–300 ms15–20x faster
Cache Hit Ratio85–95%N/AReduces origin load
TLS Handshake1–5 ms (nearby PoP)50–150 ms10–30x faster
Bandwidth Cost$0.01–0.04/GB$0.05–0.12/GB3–5x cheaper
Availability99.99% (multi-PoP)99.9% (single DC)10x fewer outages

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.

HTTP/3 Priority Levels and Scheduling Behavior HTTP/3 Extensible Priority Levels u=0 HTML document Highest priority — the page itself non-incremental u=1 Critical preloads Fonts, critical CSS referenced by preload non-incremental u=2 Render-blocking resources Stylesheets, synchronous scripts in <head> non-incremental u=3 Above-fold images LCP candidates, hero images — default urgency incremental (i) u=4 Async scripts, deferred JS Scripts with async/defer attributes non-incremental u=5 Below-fold images Lazy-loaded images, below-viewport content incremental (i) u=6-7 Prefetch, speculative preload Resources for future navigation, analytics lowest priority
Figure 1: The 8 urgency levels map cleanly to resource types. Incremental resources (images) benefit from interleaved delivery; non-incremental resources (scripts) need sequential completion

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:

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.

Stream Scheduling: Non-Incremental vs Incremental at Same Urgency Scheduling at Same Urgency Level Non-incremental (i=false) e.g., scripts — sequential delivery Stream A (complete) Stream B (complete) Stream C (complete) Each stream completes before next starts Incremental (i=true) e.g., images — interleaved delivery A chunk B chunk C chunk A chunk B chunk C chunk A chunk B chunk All streams progress simultaneously
Figure 2: Non-incremental streams are served one at a time (the client cannot use partial data). Incremental streams are interleaved so all make progress together (enabling progressive rendering)

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.

External References

References & Standards: Cloudflare documentation · RFC 7234