Network Waterfall Anatomy & Timing Metrics
Misreading the network waterfall is one of the most common sources of wasted optimization effort. The colored bars in Chrome DevTools do not simply represent “slowness” — each segment maps to a distinct browser scheduler state, protocol negotiation phase, or server-side event. This page gives you a protocol-level model of every waterfall phase, a browser-engine comparison for the states that differ across Chromium, WebKit, and Gecko, and a structured debugging workflow for diagnosing queueing stalls, TTFB inflation, and render-blocking cascades.
Concept Definition: What Each Waterfall Phase Actually Measures
The PerformanceResourceTiming API (exposed via performance.getEntriesByType('resource')) exposes start/end timestamps for each phase. Chrome DevTools renders these as coloured bars. Understanding the precise spec definition of each interval prevents misattributing server latency to network latency and vice versa.
| Phase (DevTools label) | PerformanceResourceTiming property | What it includes |
|---|---|---|
| Queueing | fetchStart − requestStart (approximation) |
Connection pool exhaustion, low-priority scheduler deferral, disk-cache lookup |
| Stalled | Contains Queueing + proxy negotiation | Everything in Queueing plus SOCKS proxy setup when applicable |
| DNS Lookup | domainLookupEnd − domainLookupStart |
Recursive DNS resolution; 0 ms when the OS or browser DNS cache hits |
| Initial Connection | connectEnd − connectStart |
TCP three-way handshake; 0 ms on reused connections |
| SSL | connectEnd − secureConnectionStart |
TLS negotiation; 0 ms on reused or pre-connected origins |
| Waiting (TTFB) | responseStart − requestStart |
Network RTT to server + server processing + queuing in the server’s send buffer |
| Content Download | responseEnd − responseStart |
Byte transfer time; dominated by payload size and bandwidth |
Browser Engine Differences
The spec defines the timing properties uniformly, but engines differ in how they populate them for cross-origin or opaque responses, and in their priority arbitration during Queueing:
| Behaviour | Chromium | WebKit (Safari) | Gecko (Firefox) |
|---|---|---|---|
Cross-origin PerformanceResourceTiming resolution |
Full timing with Timing-Allow-Origin header; otherwise all durations zero |
Full timing with Timing-Allow-Origin; otherwise zeroed |
Full timing with Timing-Allow-Origin; otherwise zeroed |
| HTTP/1.1 per-host connection limit | 6 | 6 | 6 |
fetchpriority attribute support |
Chrome 101+ | Safari 17.2+ | Firefox 132+ |
| Priority lanes for render-blocking vs. non-blocking | Yes — 5 tiers via ResourceLoadPriority |
Yes — 4 tiers | Yes — 4 tiers, slightly different weighting |
| 103 Early Hints support | Chrome 103+ | Safari 17+ | Firefox 120+ |
Spec & API Reference
Key PerformanceResourceTiming Properties
// Instrument all resource timing entries for above-the-fold analysis.
// This surfaces the raw intervals that DevTools renders as waterfall bars.
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'link' || entry.initiatorType === 'script') {
console.table({
name: entry.name.split('/').pop(),
// Queueing approximation: gap between fetchStart and connection start
queueingMs: (entry.connectStart - entry.fetchStart).toFixed(1),
dnsMs: (entry.domainLookupEnd - entry.domainLookupStart).toFixed(1),
tcpMs: (entry.connectEnd - entry.connectStart).toFixed(1),
tlsMs: entry.secureConnectionStart
? (entry.connectEnd - entry.secureConnectionStart).toFixed(1)
: 0,
// TTFB: waiting phase — includes server processing + round-trip latency
ttfbMs: (entry.responseStart - entry.requestStart).toFixed(1),
downloadMs: (entry.responseEnd - entry.responseStart).toFixed(1),
totalMs: entry.duration.toFixed(1),
});
}
}
});
observer.observe({ type: 'resource', buffered: true });
Diagnostic Thresholds
| Phase | Healthy | Investigate | Critical |
|---|---|---|---|
| Queueing / Stalled | < 10 ms | 10–50 ms | > 50 ms — likely connection pool exhaustion or priority inversion |
| DNS Lookup | 0 ms (cache) – 20 ms | 20–60 ms | > 60 ms — missing dns-prefetch or cold resolver chain |
| Initial Connection | < 80 ms | 80–150 ms | > 150 ms — TCP slow-start, geo-distance to server, or saturated uplink |
| SSL / TLS | < 30 ms | 30–80 ms | > 80 ms — session not resumed; add ssl_session_cache and TLS 1.3 |
| Waiting (TTFB) | < 200 ms | 200–600 ms | > 600 ms — server-side bottleneck or uncached edge request |
| Content Download | Proportional to size | — | > 2 s for < 100 KB signals bandwidth constraint or missing compression |
Step-by-Step Implementation
Step 1 — Eliminate Queueing with Protocol Upgrades
Long Queueing segments for assets on the same origin almost always signal HTTP/1.1 connection cap exhaustion (six simultaneous connections per host). Upgrading to HTTP/2 eliminates this limit by multiplexing all streams over a single connection. The browser’s fetch priority system then handles inter-stream ordering without queue starvation.
# nginx: Enable HTTP/2 + HTTP/3 (QUIC) on the same port.
# QUIC removes TCP head-of-line blocking between streams — see
# /http2-http3-multiplexing-connection-optimization/ for stream-weight tuning.
server {
listen 443 ssl;
listen 443 quic reuseport; # HTTP/3 via QUIC
http2 on; # HTTP/2 (nginx ≥ 1.25.1 syntax)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m; # TLS session resumption — eliminates SSL bar on repeat visits
ssl_session_timeout 1d;
add_header Alt-Svc 'h3=":443"; ma=86400' always; # advertise HTTP/3 to clients
}
Step 2 — Collapse DNS + Connection + SSL Phases with Early Hints and Preconnect
For third-party origins (fonts, analytics, CDN), the DNS + TCP + TLS triple consumes 200–400 ms on a cold connection. Declaring preconnect in the HTML <head> parallelises this setup against HTML parsing. For the primary origin, 103 Early Hints starts the connection before the server even begins generating the response body.
<!-- Preconnect to critical third-party origins.
crossorigin is required for CORS fetches (fonts, API calls) so the
browser opens a CORS-capable connection rather than a no-CORS one. -->
<link rel="preconnect" href="https://fonts.example.com" crossorigin>
<link rel="dns-prefetch" href="https://analytics.example.com">
HTTP/1.1 103 Early Hints
Link: </assets/critical.css>; rel=preload; as=style
Link: </assets/hero.webp>; rel=preload; as=image; fetchpriority=high
The 103 response is sent before the 200, so the browser can begin fetching critical.css while the server is still assembling the HTML — collapsing the entire DNS/TCP/TLS sequence into the server’s own processing time for the primary origin.
Step 3 — Attack the TTFB Phase
TTFB is the waterfall phase most engineers underestimate because it conflates two independent problems: network RTT and server processing time. Use curl to isolate them before reaching for DevTools:
# Isolate TTFB components on the command line.
# time_starttransfer = TTFB (DNS + connect + TLS + server processing + first byte RTT)
# time_connect alone = TCP handshake
# time_appconnect = TLS complete
curl -o /dev/null -s -w \
"DNS: %{time_namelookup}s\nTCP: %{time_connect}s\nTLS: %{time_appconnect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
https://example.com/
If time_starttransfer − time_appconnect (pure server processing time) exceeds 200 ms, the problem is server-side — caching, database queries, or edge routing — not the network stack.
Step 4 — Compress and Chunk to Shrink the Content Download Bar
Content Download time is responseEnd − responseStart. For text assets the dominant lever is compression; for images it is format and dimension. For JavaScript, the additional lever is render-blocking resource identification — a large app.js that blocks parsing inflates effective download time by delaying every subsequent resource.
# Brotli compression for text assets.
# brotli_comp_level 6 balances CPU cost vs compression ratio for dynamic content.
brotli on;
brotli_comp_level 6;
brotli_types text/html text/css application/javascript application/json image/svg+xml;
Verification Workflow
After applying fixes, confirm improvements with two complementary methods:
DevTools Network panel — column-level verification:
- Open DevTools → Network panel.
- Right-click any column header → enable Priority, Connection ID, and Protocol.
- Reload with cache disabled (Ctrl+Shift+R).
- Sort by Waterfall to find the longest bars; the Queueing segment (light grey) should be < 10 ms for above-the-fold resources.
- Check Connection ID — all same-origin requests should share one ID under HTTP/2, confirming multiplexing is active.
PerformanceObserver — automated CI assertion:
// Assert that no render-critical resource exceeds 50 ms of Queueing.
// Run this in a Puppeteer / Playwright test after navigation completes.
const entries = JSON.parse(
await page.evaluate(() =>
JSON.stringify(
performance
.getEntriesByType('resource')
.filter(e => e.initiatorType === 'link' || e.initiatorType === 'script')
.map(e => ({
name: e.name,
// connectStart of 0 means the connection was reused — queueing only
queueingMs: e.connectStart > 0
? e.connectStart - e.fetchStart
: e.responseStart - e.fetchStart,
}))
)
)
);
const violations = entries.filter(e => e.queueingMs > 50);
if (violations.length) throw new Error(`Queueing violation: ${JSON.stringify(violations)}`);
For decoding Chrome DevTools network waterfall segments in detail — including the Priority Inversion indicator and the hidden Disk Cache state — see the dedicated deep-dive.
Edge Cases & Gotchas
CORS and the Timing-Allow-Origin header
Cross-origin resources without Timing-Allow-Origin: * return zeroed timing properties in PerformanceResourceTiming. DevTools still shows the full waterfall visually (because it reads from the internal network log, not the JS-exposed API), but your PerformanceObserver assertions will silently under-count queueing time for third-party assets. Add Timing-Allow-Origin: * to CDN responses you control; for third-party origins, rely on DevTools or WebPageTest HAR analysis instead.
HTTP/2 priority inversion
Upgrading to HTTP/2 removes connection-count Queueing but introduces a different failure mode: a large low-priority response (a deferred analytics script, for example) can monopolise the TCP send window and starve higher-priority streams. The symptom is long Waiting (TTFB) or Content Download bars on high-priority resources, not Queueing. The fix is fetchpriority="high" on LCP images and explicit fetch priority hints — not connection tuning.
TLS 1.3 0-RTT replay risk
ssl_early_data on in nginx reduces the SSL bar to near-zero for returning visitors, but early data (0-RTT) is replayable. Restrict it to safe idempotent requests (GET, HEAD) and reject it for state-mutating endpoints via the $ssl_early_data variable.
Preload scan and dynamic insertion
The preload scanner reads raw HTML tokens before the parser executes scripts. Any resource URL that is constructed at runtime (via document.createElement or framework render cycles) is invisible to it — those resources acquire a Queueing delay equal to the script’s own execution time. This is why critical images should be declarative <img> tags or <link rel="preload"> in static HTML, not injected by JavaScript.
stale-while-revalidate and the phantom waterfall entry
When a response is served from a stale-while-revalidate cache, the browser returns the cached bytes instantly (near-zero Content Download) but issues a background revalidation fetch. DevTools shows this as a second waterfall row with a disk cache tag. The background fetch counts against connection concurrency on HTTP/1.1 — under high load it can contribute to Queueing on the visible critical path. Monitor for this with cache-control headers audits.
FAQ
Why does the waterfall show a Queueing bar even on HTTP/2?
HTTP/2 eliminates the per-host connection cap, so Queueing from connection exhaustion disappears. But the browser still queues low-priority requests when the main thread is busy executing JavaScript, and it throttles background fetches on low-power devices. Check the Priority column: if a Queueing resource has priority Low or Lowest, the browser is intentionally deferring it — that is correct behaviour, not a bug.
How do I tell whether TTFB is a server problem or a network problem?
Use the curl -w timing breakdown above to separate time_connect (pure network RTT) from time_starttransfer − time_appconnect (server processing). If server processing exceeds 100 ms, the problem is server-side. If the round-trip dominates, the problem is CDN edge placement or missing connection reuse.
Does switching to HTTP/3 remove the SSL bar in the waterfall? On the first visit to a new origin, HTTP/3 (QUIC) still performs a TLS 1.3 handshake, so the SSL bar is present. QUIC combines the transport and TLS handshakes into a single round trip (1-RTT) compared to TCP + TLS (2-RTT), so the combined Initial Connection + SSL bar should roughly halve. On repeat visits with 0-RTT session resumption, the SSL bar drops to near-zero. To understand the head-of-line blocking differences that make HTTP/3 valuable beyond just the handshake, see head-of-line blocking mitigation.
What causes a “Stalled” bar longer than the Queueing bar? Chrome reports Stalled as a superset of Queueing — it includes proxy negotiation time. If you route traffic through an HTTP/SOCKS proxy (common in corporate networks or some dev setups), Stalled will be longer than Queueing by the proxy handshake duration. In direct-connection environments they are equal.
Why does preload not always eliminate the DNS and connection phases?
<link rel="preload"> tells the browser to fetch the resource at high priority as soon as the HTML parser encounters the tag. If the preload target is on a different origin and no preconnect hint preceded it, the browser must still perform DNS + TCP + TLS for that origin — it just starts those phases earlier. Pair preconnect (for the connection setup) with preload (for the fetch) to collapse both phases simultaneously.
Related
- Decoding Chrome DevTools Network Waterfall — step-by-step panel guide for reading Queueing, Stalled, and Priority Inversion states
- Understanding Browser Resource Priority Queues — how the browser’s five-tier fetch priority system decides queue order
- Render-Blocking Resource Identification — isolating CSS and script assets that stall the critical rendering path
- Cache Interaction & Stale-While-Revalidate — how cache policies interact with waterfall depth and background revalidation fetches
- Up: Core Browser Loading Mechanics & Priority Queues