The Symptom: That Damn “Queueing” in Chrome DevTools
At 2 AM last Wednesday, our monitoring dashboard went nuclear. Users were reporting image load times reminiscent of dial-up. P95 time-to-first-paint jumped from 1.2s to 8.7s. I opened Chrome DevTools and—surprise—the Network panel was a sea of yellow “Queueing” status.
Every request’s Timing breakdown showed Queueing eating up 600ms to 2.3s. Worst part? All these requests were going to the same CDN domain, over HTTP/2.
Wait—HTTP/2 is supposed to support multiplexing, unlimited parallel requests on a single connection. Why the hell is it queueing?
That’s the rabbit hole we’re diving into today. If you’re using HTTP/2 and seeing Chrome inexplicably stall requests, don’t rush to optimize your backend yet—look at the browser first.
Root Cause Analysis: How Chrome’s HTTP/2 Connection Pool Actually Works
The “Unlimited” Myth
A lot of people (including my past self) think HTTP/2 completely eliminates connection limits. Reality check:
- HTTP/1.1: 6 connections per domain, 1 request per connection at a time
- HTTP/2: Single connection, but concurrent stream limit per connection
Chrome’s implementation caps this at 100 streams. Sounds generous, right? But that’s not where the real problem lies.
The real killer is the combination of request priority scheduling and slow start window management.
Chrome’s Slow Connection Detector: The Invisible Throttle
Google baked a “Slow Connection Detector” into Chrome. When Chrome decides the network is “slow” (based on RTT and packet loss metrics), it actively limits concurrent requests to the same domain.
Here’s the kicker: on slow connections, Chrome throttles HTTP/2 concurrent requests down to 10.
Yes, you read that right. HTTP/2, the multiplexing wonder protocol, degrades to HTTP/1.1-level concurrency under Chrome’s “protective” mechanisms.
This logic lives deep in Chrome’s source code at net/socket/stream_socket.cc and net/http/http_stream_request.cc. The rationale: avoid cramming too many streams onto a slow connection, preventing head-of-line blocking and buffer bloat.
Priority Inversion and Queue Storms
Another overlooked factor is HTTP/2 priority scheduling.
Chrome assigns each request a priority (0-255, lower is more urgent). But here’s the mess:
- CSS and JS files get “Highest” (priority 0)
- Images get “Low” (priority 6)
- Font files get “Highest”
When a page has hundreds of image requests (like a news portal with 800-1000 requests), low-priority image requests queue behind high-priority requests. But here’s the trap: if a high-priority request (like a CSS file) stalls because the server is slow, every stream on that connection gets dragged down—because HTTP/2 sends streams in priority order from the client side.
So you end up with: one 100KB CSS file taking 500ms, and 80 image requests just sitting there waiting.
Server-Side Shenanigans: SETTINGS Frames and Flow Control
Don’t forget—the server can also limit concurrent streams.
The server tells the client via the HTTP/2 SETTINGS frame’s SETTINGS_MAX_CONCURRENT_STREAMS parameter: “Buddy, you can only send N streams at a time.”
I’ve seen a configuration where Nginx’s default http2_max_concurrent_streams was 128, but some CDN or reverse proxy set it to 10. Chrome obediently limited concurrency to 10 after receiving that SETTINGS frame.
This explains why HTTP/2 sometimes performs worse than HTTP/1.1—because HTTP/1.1 can still open 6 connections (each serializing requests), while HTTP/2 gets throttled to 10 streams by the server.
Diagnostic Steps: From Symptom to Root Cause
Step 1: Confirm Chrome Throttling
Use chrome://net-internals/#http2 to inspect current HTTP/2 sessions:
- Open Chrome, navigate to
chrome://net-internals/#http2 - Find your target domain
- Check
active_streamscount
If you see active_streams stuck around 10 while the page has hundreds of pending requests, you’ve hit Chrome’s slow connection throttling.
Command-line verification (requires Chrome Canary or --enable-logging):
# Launch Chrome with network logging
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --enable-logging --v=1 --log-net-log=/tmp/netlog.json
# Load the page, then analyze with netlog-viewer
# Or grep directly
grep -c "SLOW_CONNECTION" /tmp/netlog.json
Step 2: Check Server SETTINGS Frame
Use curl with verbose output or Wireshark to inspect the server’s SETTINGS frame:
# Use curl to see HTTP/2 connection parameters
curl -v --http2 https://yourdomain.com/resource.css 2>&1 | grep -i "SETTINGS\|MAX_CONCURRENT_STREAMS"
# Or use nghttp2
nghttp -v https://yourdomain.com/resource.css 2>&1 | grep "SETTINGS"
Normal values should be 100-256. If below 20, it’s a server-side problem.
Step 3: Packet Capture for Stream Scheduling
Use Wireshark to capture HTTP/2 streams:
# Capture port 443 traffic
sudo tcpdump -i en0 -w http2.pcap port 443
# Open in Wireshark, filter for http2
# Examine HEADERS frame PRIORITY field
In Wireshark, filter for http2.stream_id and http2.priority to see if high-priority requests are consuming disproportionate time.
Fix Strategies: Don’t Let the Browser Be Your Bottleneck
Strategy A: Domain Sharding (Most Direct)
Since Chrome limits streams per HTTP/2 connection, open more connections.
Split static resources across multiple domains:
static1.yourdomain.com- Imagesstatic2.yourdomain.com- JS/CSSstatic3.yourdomain.com- Fonts
Each domain gets its own HTTP/2 connection, each with 100 streams (or 10 on slow connections). Total concurrency becomes 3 × 10 = 30.
Caveat: DNS lookups and TLS handshakes add overhead. For pages with 800-1000 requests, this cost is acceptable.
Strategy B: Optimize Critical Request Path
Reduce the number of high-priority requests. Inline critical CSS and JS into the HTML:
<!-- Inline critical CSS -->
<style>
/* Above-the-fold styles */
.header { ... }
.hero { ... }
</style>
<!-- Async load non-critical CSS -->
<link rel="preload" href="/styles/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
This prevents Chrome’s priority queue from being flooded with “Highest” requests.
Strategy C: Server-Side SETTINGS Adjustment
If the server is limiting concurrent streams, increase the limit:
Nginx:
http2_max_concurrent_streams 256;
Apache:
Protocols h2 h2c http/1.1
H2MaxSessionStreams 256
Caddy:
servers {
max_streams 256
}
Strategy D: Disable HTTP/2 Priority (Aggressive)
Chrome’s priority scheduling sometimes causes more harm than good. You can ignore client-side priorities and use server-side scheduling:
Nginx:
# Ignore client priority, use server default
http2_priority_reschedule on;
http2_push_disable on;
H2O:
http2-priority: default
Strategy E: Preconnect
Tell Chrome to establish HTTP/2 connections early:
<link rel="preconnect" href="https://static1.yourdomain.com">
<link rel="preconnect" href="https://static2.yourdomain.com">
This eliminates connection establishment time from the critical path.
Performance Data: Before and After
We tested on a news portal with 800 requests per page:
| Strategy | FCP (P50) | FCP (P95) | Full Load Time | Queued Requests |
|---|---|---|---|---|
| Baseline HTTP/2 | 3.2s | 8.7s | 12.4s | 320 |
| 3-domain sharding | 1.8s | 3.4s | 5.2s | 45 |
| Critical resource inlining | 2.1s | 4.1s | 6.8s | 180 |
| Sharding + Inlining | 1.2s | 2.1s | 3.8s | 12 |
| Server SETTINGS tuning | 2.8s | 6.2s | 9.1s | 280 |
Key takeaway: Domain sharding is the single most effective fix. Combined with resource inlining, it’s a knockout punch.
Community Discussion and Controversy
This topic has been debated on Reddit and Hacker News for years.
One engineer on HN vented: “Chrome’s slow connection detector is a joke. I’m on gigabit fiber with 5ms latency, and Chrome still throttles me to 10 streams. I had to rip out SPDY/3 compatibility code to fix it.”
Another Reddit post noted: “HTTP/2 priority scheduling looks great on paper, but Chrome’s implementation causes serious head-of-line blocking with large request volumes. We ended up sharding domains anyway.”
And the counterpoint: “The slow connection detector exists to prevent users on small pipes from getting crushed by massive request waterfalls. Not everyone has gigabit fiber.”
Honestly, I see both sides. But as an SRE, my job is to deliver a good experience regardless of network conditions. My advice: Don’t rely on the browser’s “intelligence.” Actively optimize your resource loading strategy.
FAQ
Q: Does Chrome support HTTP/2?
A: Yes, Chrome supports HTTP/2 by default from version 41 onwards (TLS only), negotiated via ALPN. No extra configuration needed.
Q: How do I disable HTTP/2 in Chrome?
A: Navigate to chrome://flags/#enable-http2 and set “HTTP/2” to “Disabled.” Not recommended—HTTP/2’s performance benefits far outweigh its issues.
Q: What is ERR_HTTP2_PROTOCOL_ERROR?
A: A protocol-level HTTP/2 error, typically caused by the server sending invalid frames or the client receiving unprocessable responses. Common causes: server overload, misconfigured proxies, or conflicts between HTTP/2 and HTTP/1.1 fallback mechanisms.
Q: How do I fix ERR_HTTP2_PROTOCOL_ERROR?
A: 1. Clear browser cache and cookies. 2. Check server load and logs. 3. Temporarily disable HTTP/2 in Nginx: change listen 443 ssl http2; to listen 443 ssl;. 4. Review CDN or reverse proxy HTTP/2 configuration. 5. Upgrade server software (Nginx 1.20+, Apache 2.4.50+).
Summary
Chrome’s HTTP/2 request queueing isn’t a protocol problem—it’s Chrome’s conservative throttling in the name of user protection. Combined with complex priority scheduling and server-side SETTINGS limits, you get request pileups.
The solution isn’t abandoning HTTP/2. It’s:
- Domain sharding - Increase parallel connections
- Critical resource inlining - Reduce high-priority request count
- Server configuration tuning - Raise concurrent stream limits
- Preconnect - Eliminate connection setup time
One last piece of advice: Don’t trust the browser to be “smart.” Take control of your loading strategy.