Connection Coalescing & Domain Sharding: Why Your Sharded CDN Is Slowing Down HTTP/2
Domain sharding was a necessary evil under HTTP/1.1: browsers allowed only six parallel connections per origin, so splitting assets across img1.example.com, img2.example.com, and static.example.com tripled effective concurrency. Under HTTP/2 and HTTP/3, the same technique does the opposite — it fragments the connection pool, defeats multiplexing, and forces redundant TLS handshakes that cost 50–150 ms each on a cold connection. This page explains how connection coalescing works at the spec level, how browsers decide when to reuse a single socket across multiple hostnames, and how to migrate a sharded CDN architecture to a coalesced one without a cache-miss spike.
How Connection Coalescing Works
Connection coalescing is the browser’s ability to route requests for two or more distinct origins over a single underlying TCP/TLS or QUIC connection. RFC 7540 §9.1.1 (HTTP/2) and RFC 9114 §3.3 (HTTP/3) both define the conditions under which this is permitted:
- Same IP address and port — both origins must resolve to the same IP:port via DNS.
- Matching ALPN token — the negotiated protocol (
h2orh3) must be identical. - Certificate SAN coverage — the TLS certificate’s Subject Alternative Name extension must list every hostname that will be coalesced over that connection.
- No security context violations — mixed content and cross-origin isolation constraints must not block the second origin.
When all four conditions hold, the browser reuses the existing connection ID rather than opening a new socket. The result: zero additional DNS lookups, no extra TLS handshake, and a single flow-control window shared across all coalesced origins.
Browser Engine Differences
Chromium, WebKit, and Gecko all implement RFC 7540 §9.1.1 coalescing, but they differ in how eagerly they apply it:
| Engine | Coalescing trigger | HTTP/3 coalescing | Notes |
|---|---|---|---|
| Chromium 120+ | Resolves IP at connection time; coalesces immediately on SAN match | Yes (h3 ALPN via Alt-Svc) |
Exposes Connection ID in DevTools Network panel |
| WebKit (Safari 17+) | Checks SAN coverage before first use; falls back to new connection on mismatch | Partial (h3 coalescing gated on QUIC version negotiation) | Does not expose Connection ID in the UI |
| Gecko (Firefox 122+) | Validates IP and SAN on each connection attempt; uses connection pool keyed on origin tuple | Yes (HTTP/3 coalescing via Alt-Svc header) | about:networking shows active HTTP/2 and HTTP/3 sessions |
The practical implication: Safari may open a second connection if a SAN mismatch is detected mid-page even for origins that share an IP — test across engines before declaring coalescing complete.
Inline SVG: Coalescing vs. Sharding Connection Topology
Spec and API Reference
Coalescing Prerequisite Checklist
| Requirement | What to verify | Command |
|---|---|---|
| Same resolved IP | Both origins return the same A/AAAA record | dig +short cdn.example.com static.example.com |
| Same port | Both served on 443 | curl -sv https://static.example.com 2>&1 | grep "Connected to" |
ALPN h2 or h3 |
Negotiated protocol token | openssl s_client -alpn h2 -connect cdn.example.com:443 2>&1 | grep "Protocol" |
| SAN coverage | All coalesced hostnames in cert SAN | openssl s_client -connect cdn.example.com:443 2>/dev/null | openssl x509 -noout -text | grep -A2 "Subject Alt" |
| No mixed-content | All subresources served over HTTPS | Chrome DevTools Console → filter “mixed content” |
Browser Support Matrix
| Feature | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| HTTP/2 connection coalescing | 43+ | 36+ | 10.1+ | 14+ |
| HTTP/3 connection coalescing | 87+ | 88+ | 16.4+ | 87+ |
| DevTools Connection ID column | 67+ | Not exposed | Not exposed | 67+ |
Alt-Svc h3 upgrade |
87+ | 88+ | 16.4+ | 87+ |
| 0-RTT QUIC session resumption | 87+ | 88+ | 16.4+ | 87+ |
Step-by-Step Implementation
Step 1 — Map and Consolidate Active Origins
Audit every asset hostname in use. Search HTML, CSS, JS, and HTTP response headers:
# Find all external hostnames referenced in the built output
# This surfaces img1/img2/static shards that need consolidation
grep -rhoP 'https?://[^/"]+' dist/ | sort -u | grep '\.example\.com'
Target: reduce to at most two asset origins — one primary CDN hostname and one optional secondary for a geographically isolated region. Every additional origin above that creates a new TLS handshake budget that HTTP/2 and HTTP/3 multiplexing cannot recover.
Step 2 — Issue a Multi-SAN Certificate
A single certificate must cover all consolidated hostnames in its SAN extension. With Certbot (Let’s Encrypt):
# Issue one cert covering the primary CDN and its coalesced aliases.
# All listed domains must resolve to the same IP for coalescing to work.
certbot certonly --nginx \
-d cdn.example.com \
-d static.example.com \
-d assets.example.com \
--preferred-challenges dns-01 # DNS challenge avoids per-hostname HTTP validation
Verify the resulting certificate covers every hostname before deploying:
# Confirm SAN list — all coalesced hostnames must appear here
openssl x509 -noout -text -in /etc/letsencrypt/live/cdn.example.com/fullchain.pem \
| grep -A4 "Subject Alternative Name"
# Expected: DNS:cdn.example.com, DNS:static.example.com, DNS:assets.example.com
Step 3 — Enforce ALPN and Advertise HTTP/3
Configure your reverse proxy to negotiate h2 and h3 and emit the Alt-Svc header so browsers can upgrade to QUIC on subsequent visits:
# nginx — serve all coalesced aliases on a single server block
# ALPN h2 is declared via 'http2' directive; h3 via Alt-Svc header
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
listen 443 quic reuseport; # HTTP/3 / QUIC listener (nginx ≥ 1.25.0)
listen [::]:443 quic reuseport;
# All coalesced hostnames share this block and this certificate
server_name cdn.example.com static.example.com assets.example.com;
ssl_certificate /etc/letsencrypt/live/cdn.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cdn.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_tickets on; # Enables TLS 1.2 session resumption
ssl_stapling on; # Reduces OCSP round-trip for clients
# Advertise HTTP/3 so browsers can coalesce over QUIC on next visit.
# ma=86400 means browsers cache this upgrade hint for 24 h.
add_header Alt-Svc 'h3=":443"; ma=86400, h3-29=":443"; ma=86400';
# Early Hints for render-critical CSS (sent before the full response)
add_header Link '</css/critical.css>; rel=preload; as=style';
}
Step 4 — Remove Legacy Sharding in Application Code
Find and rewrite all hardcoded shard hostnames in the application layer before updating DNS:
// Before: assets split across three shard hostnames (HTTP/1.1 legacy pattern).
// This forces three separate TLS handshakes under HTTP/2 — net negative.
const IMAGE_BASE = 'https://img1.example.com';
const FONT_BASE = 'https://img2.example.com';
const SCRIPT_BASE = 'https://static.example.com';
// After: all assets on a single consolidated origin.
// The browser coalesces any remaining hostname aliases automatically via SAN.
const ASSET_BASE = 'https://cdn.example.com';
For build-tool-based projects, update the asset prefix in a single place:
// vite.config.js — centralise the CDN base so shards cannot re-appear
export default {
base: 'https://cdn.example.com/', // All emitted asset URLs use this prefix
build: {
rollupOptions: {
output: {
// Predictable chunk names make cache fingerprinting stable post-consolidation
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash][extname]',
}
}
}
};
Step 5 — Trim Redundant preconnect Hints
After consolidation, preconnect hints for sharded hostnames are waste. Remove them and keep only hints for origins that genuinely cannot be coalesced — third-party analytics, payment iframes, and font providers:
<!-- Remove: preconnect to former shards is now wasted DNS+TLS -->
<!-- <link rel="preconnect" href="https://img1.example.com"> -->
<!-- <link rel="preconnect" href="https://img2.example.com"> -->
<!-- Keep: third-party origins that cannot coalesce with your CDN -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Add fetchpriority to LCP image to prevent stream starvation on the shared connection -->
<img src="https://cdn.example.com/hero.webp" fetchpriority="high" alt="Hero image">
Verification Workflow
Chrome DevTools (Recommended)
- Open DevTools → Network panel.
- Right-click any column header → enable Connection ID and Protocol.
- Hard-reload (
Ctrl+Shift+R) to bypass the cache and see a clean waterfall. - Filter by
domain:static.example.comthendomain:cdn.example.com. If coalescing is working, both sets of requests will share the same Connection ID number. - Sort by Waterfall. Under HTTP/2 or HTTP/3, the TCP and TLS bars should appear only once at the top — not repeated for each hostname.
NetLog Deep Inspection
For cases where DevTools does not expose connection IDs clearly (e.g. Safari, Firefox):
# Chrome only
1. Navigate to chrome://net-export/
2. Click "Start Logging to Disk" and record a page load.
3. Open the downloaded JSON in https://netlog-viewer.appspot.com (or the local viewer).
4. Filter event type = HTTP2_SESSION or QUIC_SESSION.
5. Verify that requests for cdn.example.com and static.example.com both appear
under the same session ID.
PerformanceObserver RUM Check
Deploy this snippet to confirm in field data that connection overhead has dropped after consolidation:
// Measure DNS + TLS overhead per resource entry.
// After coalescing, domainLookupEnd - domainLookupStart and
// connectEnd - connectStart should be 0 for all but the first request.
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const dns = entry.domainLookupEnd - entry.domainLookupStart;
const tls = entry.connectEnd - entry.connectStart;
if (dns > 0 || tls > 0) {
// Non-zero values indicate a new connection was opened for this resource.
// Investigate: SAN mismatch, IP mismatch, or CDN routing anomaly.
console.warn(`New connection for ${entry.name}: DNS=${dns}ms TLS=${tls}ms`);
}
}
});
observer.observe({ type: 'resource', buffered: true });
Lighthouse CI Budget
Enforce connection hygiene in CI by failing builds that regress:
# .lighthouserc.json — cap DNS lookups and connections
# 'uses-http2' audit fails if any resources are served over HTTP/1.1
{
"ci": {
"assert": {
"assertions": {
"uses-http2": ["error", { "minScore": 1 }],
"network-requests": ["warn", { "maxNumericValue": 30 }]
}
}
}
}
Edge Cases & Gotchas
CDN Routing Inconsistency Breaks Coalescing
Anycast CDNs route each request to a nearby PoP. If cdn.example.com and static.example.com resolve to different PoPs (different IPs), the browser will not coalesce them — the IP match requirement fails. Fix: pin both CNAMEs to the same CDN origin hostname, or use a CDN that guarantees consistent IP resolution for coalesced aliases from the same PoP.
Certificate Mismatch Mid-Session
If you add a hostname to the SAN after users already have active connections, the browser’s in-memory certificate cache may still hold the old cert without the new SAN. Users need a fresh connection (tab close, or a cleared socket pool via chrome://net-internals/#sockets) before coalescing activates for the new hostname. Plan SAN updates during low-traffic windows.
HTTP/2 Stream Contention on the Shared Connection
Coalescing all origins into one connection means head-of-line blocking at the application layer still applies under HTTP/2 if stream weights are not configured correctly. A low-priority analytics payload can delay a high-priority LCP image if the server sends them in the wrong order. Assign correct HTTP/2 stream prioritization weights and use fetchpriority attributes to signal urgency to the browser scheduler.
Service Worker Coalescing Interactions
A fetch event handler that rewrites request URLs before they hit the network can break coalescing if it changes the hostname to one not covered by the current connection’s certificate. Audit your Service Worker’s fetch handler to ensure URL rewrites preserve the consolidated origin, or explicitly open a new connection via cache: 'no-store' to avoid a stale connection ID being used with a mismatched SAN.
Vary Header After Consolidation
When you collapse shards onto a single origin, CDN cache keys that previously depended on the hostname now need another differentiator. Ensure Vary: Accept-Encoding is present and consider adding Vary: Accept if you serve different formats (WebP vs JPEG) from the same URL structure. Missing or incorrect Vary headers after consolidation can cause content negotiation failures.
HTTP/3 and Alt-Svc Rollout Timing
The Alt-Svc header advertising h3 is only respected after the browser receives it — the first connection always uses TCP. This means a new user’s first page load will not benefit from QUIC coalescing; only subsequent visits within the ma (max-age) window will. For sites with high new-visitor traffic, the expected h3 coalescing gain should be measured against returning visitors separately.
FAQ
Does connection coalescing work across different registrable domains?
No. Coalescing requires the same IP address, port, ALPN token, and a certificate whose SAN covers both origins. Different registrable domains (example.com vs example.net) cannot coalesce even if they resolve to the same IP, because the certificate validation step will fail for the second origin.
Does domain sharding still make sense under HTTP/3 with QUIC?
No. QUIC eliminates TCP-level head-of-line blocking at the transport layer, which was the primary justification for sharding under HTTP/1.1. Sharding under QUIC forces multiple QUIC handshakes, wastes 0-RTT session resumption tokens, and prevents the browser from applying unified stream prioritization across all asset requests.
Will removing sharding break my CDN cache?
Possibly. If your CDN uses the Host header as part of its cache key, assets previously cached under img1.example.com will be a cache miss when served from cdn.example.com, even if the object is the same. Warm the cache on the new origin before cutting over DNS, and confirm your CDN’s cache key configuration includes the correct Vary directives post-consolidation.
Can I use preconnect hints alongside connection coalescing?
Use preconnect only for origins that genuinely cannot coalesce — third-party APIs, analytics endpoints, or font CDNs. For origins that will coalesce with your primary CDN hostname, a preconnect hint causes an unnecessary DNS lookup and TLS handshake on a connection the browser will discard the moment it detects the coalescing opportunity. See strategic preconnect and dns-prefetch usage for the decision criteria.
How do I verify coalescing in Chrome DevTools?
Open the Network panel, right-click the column headers, and enable Connection ID. Requests from different hostnames sharing the same Connection ID number are being coalesced. For deeper inspection, record a chrome://net-export/ log and filter for HTTP2_SESSION or QUIC_SESSION events — coalesced requests appear under the same session object.
Related
- HTTP/2 Stream Prioritization & Weighting — assign correct stream weights once coalescing puts all assets on a shared connection
- Mitigating Head-of-Line Blocking — how HTTP/2 application-level HOLB differs from the TCP-level problem coalescing cannot solve
- CDN Edge Tuning for QUIC & HTTP/3 — configure Alt-Svc, 0-RTT, and QUIC connection migration at the edge
- Strategic Preconnect & DNS-Prefetch Usage — decide which third-party origins still warrant a preconnect hint after consolidation