C.W.K.
Stream
Lesson 05 of 05 · published

HTTP/2 Multiplexing — What Changes for Client Design

~10 min · caching-perf, http2, multiplexing, client-design

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"A decade of 'reduce HTTP requests' advice was a workaround for HTTP/1.1's per-connection cost. HTTP/2 multiplexing changes the math. Some old best practices are now active anti-patterns."

The HTTP/1.1 Constraint That Shaped Everything

Browsers historically opened up to ~6 parallel TCP connections per origin. Past that, requests queued. To paint a page with 60 images, the browser had three choices: open more origins (sharding), inline the images, or concatenate everything into mega-bundles.

The era of front-end optimization that followed — CSS sprite sheets, image inlining, JS bundling, domain sharding — was almost entirely a response to this constraint. Reduce request count → reduce connection cost → faster pages.

What HTTP/2 Changed

One TCP connection per origin, with many concurrent streams multiplexed over it. The per-request cost evaporates. Hundreds of small assets fly down the wire in parallel. Header compression (HPACK) deduplicates repeated request headers across the same connection.

The consequence for client design:

  • Granular assets win. Concatenating everything into one big bundle now HURTS — one byte change invalidates the whole bundle for every user. Smaller files cache more efficiently and allow incremental updates.
  • Sharding hurts. Splitting assets across multiple subdomains required separate TCP+TLS handshakes per subdomain — a win in HTTP/1.1, an active waste in HTTP/2.
  • Inlining hurts. Inlining CSS or images into HTML made sense when a request cost a connection. Now an inlined asset can't be cached separately, so any HTML change re-sends it.
  • Image sprites are obsolete. Same logic — better to serve individual images that cache and update independently.

What Still Matters in HTTP/2

  • Compression — still huge wins; orthogonal to multiplexing.
  • Caching — still the most important optimization; orthogonal.
  • Asset sizes — still matter; HTTP/2 makes 50 requests fast, but each one still has a body to transfer.
  • Critical-path rendering — minimize what's needed to render the first paint.
  • Connection priority — HTTP/2 supports per-stream priority. Browsers use this to prioritize critical CSS over offscreen images.
Optimizations are workarounds for the constraints of their era. When the constraint changes, the workaround becomes the problem. HTTP/2 didn't just speed things up — it inverted the cost model. Old advice still in tutorials is now actively wrong; revisit your build pipeline if it was set up before 2020.

The HTTP/2 Practical Checklist

  • Serve over HTTP/2. Your server or CDN should negotiate it automatically; verify with curl --http2 -v.
  • Drop bundle-everything builds. Modern Vite/esbuild/Rollup support fine-grained chunking; let them.
  • Stop sharding. One origin per service. Subdomain sharding actively hurts.
  • Use ETags / Cache-Control aggressively. Per-asset, granular cache invalidation now beats bundle-busting.
  • Critical CSS inlined sparingly. Inline only the <8KB needed for first paint; load the rest via cacheable file.

The Head-of-Line Blocking Caveat

HTTP/2's multiplexing happens at the HTTP layer, but TCP is still underneath. A single dropped packet stalls the entire TCP connection — and therefore every stream multiplexed over it — until retransmission. On lossy networks (mobile, congested Wi-Fi) this hurts. HTTP/3 (over QUIC) fixes this by handling loss per-stream, so other streams keep flowing. For mobile-heavy traffic, HTTP/3 is the next migration step.

cwkPippa's HTTP/2 Reality

cwkPippa's backend serves HTTP/1.1 from Uvicorn — fine for local + Tailscale where the multiplexing wins are tiny (always one client at a time on a fast LAN). Static assets from Vite are already chunked into many small files, so even on HTTP/1.1's 6-parallel limit they download fast. cwk-site on Vercel gets HTTP/2 and HTTP/3 from the edge automatically; the Next.js build output assumes HTTP/2 multiplexing (lots of small chunks). If we ever ship cwkPippa publicly, HTTP/2 from a reverse proxy (nginx, Caddy, or Cloudflare in front of Uvicorn) is a one-day migration with no application changes.

Code

Verify HTTP/2 + measure the parallel-request win·bash
# Verify a server speaks HTTP/2
curl -v --http2 -o /dev/null https://creativeworksofknowledge.com/ 2>&1 | grep -E '(HTTP|h2)' | head
# Look for:
# * Connected to ... via HTTP/2
# < HTTP/2 200

# Force HTTP/1.1 for comparison
curl -v --http1.1 -o /dev/null https://creativeworksofknowledge.com/ 2>&1 | grep HTTP | head
# < HTTP/1.1 200 OK

# Measure the difference: parallel asset fetches
ASSETS=$(for i in {1..6}; do echo https://creativeworksofknowledge.com/asset$i.png; done)
time curl -s --http1.1 -o /dev/null $ASSETS    # 6 separate TCP connections (sequential or capped parallel)
time curl -s --http2     -o /dev/null $ASSETS  # multiplexed over one connection
60 parallel requests: HTTP/1.1 sequential vs HTTP/2 multiplexed·python
# Python httpx — opt in to HTTP/2 + reuse connection
import httpx
import time

# 60 parallel asset requests — perfect HTTP/2 multiplexing demo
URLS = [f'https://creativeworksofknowledge.com/asset_{i}.png' for i in range(60)]

# Sequential HTTP/1.1 — limited by per-connection RTT
start = time.perf_counter()
with httpx.Client() as c:  # HTTP/1.1 by default
    for url in URLS:
        c.get(url)
print(f'sequential HTTP/1.1: {(time.perf_counter() - start):.1f}s')

# Concurrent HTTP/2 — multiplexed over one connection
import asyncio

async def fetch_all():
    async with httpx.AsyncClient(http2=True) as c:
        return await asyncio.gather(*[c.get(url) for url in URLS])

start = time.perf_counter()
asyncio.run(fetch_all())
print(f'concurrent HTTP/2:   {(time.perf_counter() - start):.1f}s')
Vite config: granular chunks (the HTTP/2-friendly default)·javascript
// Modern Vite config — small chunks, NOT one big bundle (HTTP/2-friendly)
// vite.config.js
export default {
  build: {
    target: 'es2022',
    rollupOptions: {
      output: {
        // Split vendor chunks per major library — they cache forever, app code changes don't bust them
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
          'router':       ['react-router'],
          'state':        ['zustand', 'jotai'],
          // ... small, focused chunks
        },
      },
    },
  },
};

// HTTP/2 makes this strategy win: each chunk is a separate file, cached independently.
// A bug fix in app code doesn't bust the 200KB react-vendor chunk.
// Without HTTP/2, you'd want one big bundle to minimize requests; with it, granular wins.

External links

Exercise

Pick a real HTTP/2-supporting site (any modern site fronted by Cloudflare/Fastly/Vercel). Fetch its homepage twice with curl: once with --http1.1, once with --http2. Count how many TCP connections were opened (look for 'Trying' lines) and time the full fetch. Then download every asset on the page (find the asset URLs in the HTML), once per protocol. The HTTP/2 fetch should be dramatically faster on the asset bundle. Bonus: open Chrome DevTools' Network panel on a real site with HTTP/2, look at the 'Protocol' column — it should say 'h2' for everything.
Hint
The 'Trying X.X.X.X:443' lines in curl -v tell you how many TCP connections were opened. HTTP/1.1 opens up to N (curl caps at 1 by default with --parallel; browsers cap at 6); HTTP/2 opens 1 and multiplexes. Chrome DevTools 'Protocol' column is the easiest visual confirmation. The asset-bundle benchmark is the most dramatic — pages with 50+ assets show the HTTP/2 win clearly.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.