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:

  1. Same IP address and port — both origins must resolve to the same IP:port via DNS.
  2. Matching ALPN token — the negotiated protocol (h2 or h3) must be identical.
  3. Certificate SAN coverage — the TLS certificate’s Subject Alternative Name extension must list every hostname that will be coalesced over that connection.
  4. 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

Domain sharding vs connection coalescing topology Left side shows a browser opening three separate TCP+TLS connections to three sharded hostnames under HTTP/1.1. Right side shows a single TCP+TLS connection to one origin serving all three hostnames via coalescing under HTTP/2 or HTTP/3. HTTP/1.1 — Domain Sharding Browser 6 conn / origin img1.example.com img2.example.com static.example.com TCP+TLS #1 TCP+TLS #2 TCP+TLS #3 3x DNS lookups 3x TLS 1.3 handshakes (~150 ms each cold) Fragmented stream priority signals No 0-RTT session resumption across shards Cache partitioned per hostname HTTP/2 / HTTP/3 — Coalescing Browser 1 connection cdn.example.com multi-SAN cert 1x TCP+TLS / QUIC Stream 1: img1/ → Stream 3: img2/ → Stream 5: static/ → all on Connection ID #4 1x DNS + 1x TLS handshake 0-RTT session resumption available Unified stream priority weighting Shared flow-control window

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

  1. Open DevTools → Network panel.
  2. Right-click any column header → enable Connection ID and Protocol.
  3. Hard-reload (Ctrl+Shift+R) to bypass the cache and see a clean waterfall.
  4. Filter by domain:static.example.com then domain:cdn.example.com. If coalescing is working, both sets of requests will share the same Connection ID number.
  5. 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.


Up: HTTP/2 & HTTP/3 Multiplexing & Connection Optimization