HTTP/2 Stream Prioritization & Weighting

HTTP/2 multiplexes every asset over a single connection, which solves the connection-limit problem of HTTP/1.1 — but creates a new one: the server must decide which stream to send bytes for first. Stream prioritization is the protocol’s answer. When poorly configured, critical CSS and the LCP image wait behind analytics and web-font variants, adding hundreds of milliseconds to render. This page explains the priority model precisely, maps it to what each browser engine actually signals, and gives you a step-by-step implementation using RFC 9218 — the modern replacement for the deprecated PRIORITY frame approach.


Concept Definition: Dependency Trees, Weights, and RFC 9218

The RFC 7540 Weight and Dependency Model (Legacy — Diagnostic Context Only)

RFC 7540 §5.3 introduced a tree-based priority system. Each stream names a parent stream ID and declares an integer weight from 1 to 256. The server allocates available send bandwidth proportionally: a weight-256 stream gets 16× the bytes of a weight-16 stream when both are ready to receive. An exclusive flag (E) lets a new stream insert itself between a parent and all its existing children, re-rooting the subtree.

In practice this model was rarely implemented correctly. Server implementations disagreed on how to interpret the dependency tree, intermediaries stripped or corrupted PRIORITY frames, and the overhead of maintaining the tree added latency without reliable benefit. RFC 9113 (the HTTP/2 revision published in 2022) deprecated the entire PRIORITY frame mechanism. New deployments must not rely on it. The table below covers it purely for engineers reading legacy server logs or debugging old infrastructure.

Field Bits Values / Notes
Stream ID 31 The stream being prioritised
E — Exclusive flag 1 1 = insert as exclusive child of parent, reparenting siblings
Stream Dependency 31 Parent stream ID; 0 = root-level (no parent)
Weight 8 Wire value is weight − 1; 0x0F on wire = logical weight 16; range 1–256

RFC 9218: The Current Standard

RFC 9218 defines an extensible HTTP priority scheme that works with both HTTP/2 and HTTP/3. It replaces tree-and-weight with two orthogonal signals on a single Priority header:

  • u — urgency: integer 0 (highest) to 7 (lowest). Default 3.
  • i — incremental: boolean. When set, the response can be interleaved byte-by-byte with other responses (useful for streaming). When absent, the server should deliver the response in one contiguous burst.

Browsers send Priority on each request to tell the server what urgency they need. Servers can also send Priority on responses (or in PUSH_PROMISE) to hint at sequencing for pushed resources, though server push is itself deprecated.


Browser Engine Differences

The three major engines map their internal fetch priority tiers to RFC 9218 urgency values differently. Understanding the mapping prevents surprises when an audit shows unexpected urgency values in the wire traffic.

Browser Priority Mapping to RFC 9218 Urgency A table-style diagram showing how Chromium, WebKit, and Gecko each map resource types — render-blocking CSS, LCP image, sync script, async script, and lazy image — to RFC 9218 urgency levels 0 through 7. Resource Type Chromium WebKit / Safari Gecko / Firefox Render-blocking CSS u=0 u=0 u=0 LCP image (fetchpriority=high) u=1 u=1 u=2 Synchronous (parser-blocking) script u=2 u=1 u=2 Async / deferred script u=4 u=3 u=4 Lazy-loaded image (loading=lazy) u=6 u=5 u=6 Prefetch / speculative load u=7 u=7 u=7 Urgency scale u=0 highest u=2 u=4 u=6 u=7 lowest Sources: Chromium http_stream_priority.h; WebKit ResourceLoadPriority.h; Firefox nsContentUtils.cpp — values as of 2026. Note: priorities shift dynamically (e.g. image entering viewport is promoted). These are initial values at request time.

The most important cross-engine difference to know: Gecko maps fetchpriority=high images to u=2 rather than u=1, so on Firefox the LCP image competes with parser-blocking scripts rather than sitting above them. Use <link rel="preload" as="image" fetchpriority="high"> to ensure the browser signals the highest urgency it allows for images across all three engines.


Spec/API Reference: RFC 9218 Priority Header

Syntax

Priority: u=0, i

u ranges from 0 (highest urgency, deliver first) to 7 (lowest, background fetch). The i (incremental) flag is a boolean — present means the server may interleave the response bytes with other streams. Absent means the server should treat the response as a single contiguous unit.

Browser Support Matrix

Feature Chrome Safari Firefox Edge
RFC 9218 Priority request header 101+ 17.2+ 120+ 101+
fetchpriority attribute on <img>, <link>, <script> 102+ 17.2+ 132+ 102+
fetch({ priority: 'high' }) 73+ 17.4+ 132+ 79+
Server Priority response header honoured Partial Partial Partial Partial
PRIORITY frame (RFC 7540) Deprecated Deprecated Deprecated Deprecated

“Partial” for the server-sent Priority response header reflects that browsers vary in whether they adjust re-fetch scheduling based on what the origin signals — the header is not yet uniformly acted on for response-driven hint propagation.

fetchpriority on HTML Elements

The fetchpriority attribute ("high" / "low" / "auto") instructs the browser to adjust the urgency it signals on the outgoing request. It does not directly set a numeric urgency — each engine maps the three values to its internal tier, which then maps to an RFC 9218 urgency level as shown in the diagram above.

<!-- Raise urgency for the above-the-fold hero image — signals u=1 in Chromium -->
<img src="/hero.webp" fetchpriority="high" alt="">

<!-- Preload the LCP image before the parser reaches <body> -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">

<!-- Explicitly lower urgency for a below-fold decorative image -->
<img src="/deco.webp" fetchpriority="low" loading="lazy" alt="">

Step-by-Step Implementation

Step 1 — Inventory Current Priority Signals

Before changing any configuration, map what the browser is actually requesting. Open Chrome DevTools → Network tab → reload with cache disabled. Right-click any column header and enable the Priority column. Look for assets in the critical path (render-blocking CSS, fonts, LCP image) that show Low or Medium — these are candidates for elevation.

Export a HAR file (right-click → Save all as HAR with content) to feed into the later verification steps.

Step 2 — Add fetchpriority to the LCP Image

The single highest-ROI change for most sites. The browser’s preload scanner discovers <img> tags, but without fetchpriority="high" it may assign u=3 (medium urgency) until it parses layout and confirms the image is above the fold.

<!--
  fetchpriority="high" maps to u=1 in Chromium and u=2 in Gecko.
  Combined with preload, this ensures the request is issued at the
  highest urgency the browser allows before layout is complete.
-->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high"
      imagesrcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero.webp 1200w"
      imagesizes="100vw">

Step 3 — Inject RFC 9218 Priority Headers on the Server

Configure the origin to send Priority headers so that CDN edge nodes, proxies, and HTTP/2 servers that support RFC 9218 can schedule responses accordingly.

# Nginx — add Priority response headers per asset tier.
# u=0: render-blocking resources that directly affect FCP.
location ~* \.(css)$ {
  add_header Priority "u=0" always;  # render-blocking: highest urgency
  expires 30d;
}

# u=1: critical image assets identified as LCP candidates.
location ~* \.(webp|avif|jpg|png)$ {
  add_header Priority "u=1, i" always;  # i=incremental: bytes can interleave
  expires 30d;
}

# u=2: synchronous JavaScript that may affect TTI.
location ~* \.js$ {
  add_header Priority "u=2" always;
  expires 7d;
}

# u=4: non-critical scripts and secondary JSON payloads.
location ~* \.(json|xml)$ {
  add_header Priority "u=4, i" always;
  expires 1h;
}

# u=6: web fonts — important but not render-blocking if preloaded correctly.
location ~* \.(woff2|woff)$ {
  add_header Priority "u=6, i" always;
  expires 365d;
}

Step 4 — Elevate Critical Fetches in JavaScript

For dynamically loaded resources where you cannot use HTML attributes, the Fetch API accepts a priority option that maps to the same internal urgency tiers.

// fetchpriority='high' signals u=1 in Chromium for the data payload
// that drives the above-fold rendering. Without it, the default fetch
// priority is 'auto' which typically maps to u=3.
const criticalData = await fetch('/api/above-fold-content', {
  priority: 'high', // RFC 9218 u=1 in Chromium; u=2 in Gecko
});

// 'low' for analytics or background sync — keeps u=5 or u=6
// so it does not compete with render-critical streams.
fetch('/analytics/beacon', {
  method: 'POST',
  priority: 'low',
  body: JSON.stringify(payload),
  keepalive: true,
});

Step 5 — Consolidate Origins to Preserve Priority Continuity

Connection coalescing ensures that priority signals from one origin’s stream dependency context remain coherent. Assets split across many subdomains each start their own connections — the server can only schedule priority relative to streams on the same connection. Serving all critical assets from one origin (or coalesced origins sharing a TLS certificate and IP) lets the server apply u=0 vs u=4 scheduling across the full set of in-flight requests.


Verification Workflow

DevTools Priority Column

  1. Open Chrome DevTools → Network tab.
  2. Right-click any column header → enable Priority.
  3. Reload with cache disabled (Ctrl+Shift+R).
  4. Filter by type (Img, CSS, JS) and check that render-blocking assets show Highest and deferred assets show Low or Lowest.
  5. Hover a resource row to see the tooltip — Chrome shows both the initial and final priority. A change from Low → Highest means the browser promoted the resource mid-flight (e.g. an image scrolled into view), which is a sign fetchpriority="high" should have been set from the start.

PerformanceObserver Baseline and Post-Change Comparison

// Collect LCP and FCP timestamps before and after priority changes.
// Run this snippet in DevTools console on a throttled (Slow 3G) connection
// to amplify the effect of priority differences.
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(
      `${entry.entryType}: ${entry.name || ''} @ ${entry.startTime.toFixed(1)}ms`
    );
    // Log the LCP element so you can confirm it is the right image
    if (entry.entryType === 'largest-contentful-paint') {
      console.log('LCP element:', entry.element);
    }
  }
});
observer.observe({
  type: 'largest-contentful-paint', buffered: true,
});
observer.observe({
  type: 'paint', buffered: true,  // captures FCP
});

Record the baseline LCP time, apply the fetchpriority="high" change and Priority headers, then compare on a throttled connection. A well-configured priority setup typically improves LCP by 100–300 ms on mobile 3G.

HAR Analysis for Urgency Verification

Export a HAR file and inspect the _priority field on each request entry. In Chrome’s HAR format, values map as follows: Highestu=0/u=1, Highu=2, Mediumu=3, Lowu=4/u=5, Lowestu=6/u=7. Cross-reference _priority against _stream_id and startedDateTime to identify contention windows where low-urgency streams are sending bytes before high-urgency streams.


Edge Cases & Gotchas

CDN Stripping of the Priority Header

Many CDN edge nodes silently drop the Priority request header before forwarding to origin, or strip it from cached responses before delivery. Verify end-to-end passthrough with:

# Capture frames at origin — confirm Priority header arrives
tcpdump -i eth0 -w priority-capture.pcap port 443
# Then open in Wireshark: filter http2.header.name == "priority"
# and verify the value matches what the browser sent

Compare the captured value at origin with what you see in DevTools. If they differ, your CDN is rewriting or dropping the header.

fetchpriority Does Not Override Connection-Level Flow Control

fetchpriority="high" raises the urgency the browser signals — it does not bypass TCP or QUIC flow control windows. If the connection’s congestion window (cwnd) is exhausted (common in the first round-trip on a new connection), even a u=0 stream will stall. Mitigate by ensuring TLS 1.3 with 0-RTT or session resumption so the connection window from a prior session is available immediately, and by keeping critical resources small enough to fit within the initial cwnd (~14 KB for the initial flight, ~100 KB after a few round-trips).

Exclusive Dependency Flag and Tree Corruption (Legacy Servers Only)

On servers still processing RFC 7540 PRIORITY frames, the exclusive flag (E=1) inserts the new stream between a parent and all of its current children. A misbehaving client that sets E=1 on every request can flatten the dependency tree to a single chain, serialising what should be parallel streams. If you are maintaining infrastructure that uses explicit PRIORITY frames, audit the E flag distribution in Wireshark (http2.priority.exclusive == 1 filter) and verify the server’s resulting tree structure.

HTTP/2 Push Deprecation

Server Push (PUSH_PROMISE frames in HTTP/2, and the Nginx http2_push_preload directive) is deprecated. Chrome removed push support in 106. Use 103 Early Hints with Link: <…>; rel=preload instead — it achieves similar goals without the stream complexity, and it interacts cleanly with the browser’s resource priority queues.

Priority Inversion Across CDN Hops

When a request traverses multiple CDN tiers (origin shield → edge PoP → client), each hop may re-queue streams independently. A u=0 asset requested by the edge may be served to the edge at u=0, but the edge may forward it to the client at default urgency if the CDN does not propagate priority metadata across its internal routing. For a detailed root-cause and fix guide see fixing HTTP/2 priority inversion issues.

CORS and fetchpriority

The Priority header is a CORS-safelisted request header as of mid-2023, so cross-origin requests include it without triggering a preflight. However, if your CDN or origin inspects Priority to make caching decisions, ensure the Vary response header includes Priority to prevent a u=0 cached response from being served to a u=5 request (or vice versa) — which would break downstream scheduling heuristics.


FAQ

Are HTTP/2 PRIORITY frames still useful in 2026?

PRIORITY frames (RFC 7540 §5.3) are deprecated in RFC 9113 and ignored by most modern servers. Use RFC 9218 Priority headers instead; they work with both HTTP/2 and HTTP/3 connections without the binary-frame overhead or tree-maintenance cost.

Does Chrome respect the Priority header automatically?

Chrome maps internal fetch priority tiers to RFC 9218 urgency values when sending requests, but does not read the server’s Priority response header to change its own subsequent scheduling. The Priority header in requests is what drives server-side scheduling; the response-side header is a hint for intermediaries, not a directive back to the browser.

Can CDNs strip Priority headers?

Yes. Many CDN edge nodes do not forward the Priority request header to origin, or do not preserve it on cached responses. Verify passthrough with tcpdump at the origin and compare against the edge-layer HAR. Check your CDN’s documentation for a “pass all request headers” or “forward custom headers” setting.

What is the default urgency value browsers use for CSS?

Chromium signals u=0 for render-blocking CSS loaded in <head>. WebKit and Gecko also assign their highest urgency tier to render-blocking stylesheets, which maps to u=0. CSS loaded asynchronously (e.g. via media="print" flip trick or rel=preload with as=style) may start at u=1u=2 until the browser determines whether it is render-blocking.

How do I fix priority inversion where a low-priority asset loads before my LCP image?

See fixing HTTP/2 priority inversion issues — it covers the three most common root causes: fetchpriority missing on the LCP image, CDN stripping of Priority headers, and scheduler contention caused by too many u=0 assets competing for the same connection window.