HTTP/2: The Modern Web Protocol
Multiplexing: Concurrent Streams on a Single Connection
Read aloud is unavailable in this browser.
The Multiplexing Breakthrough
If binary framing is HTTP/2's foundation, multiplexing is its crown jewel. Multiplexing solves the most fundamental performance problem in HTTP/1.1: the inability to efficiently use network connections for concurrent requests.
In HTTP/1.1, web browsers open 6-8 parallel connections per domain, each carrying one request at a time. Even with connection reuse, this creates artificial bottlenecks. HTTP/2 multiplexing eliminates this constraint entirely—a single connection can carry hundreds of concurrent requests and responses, interleaved at the frame level.
This isn't incremental improvement. It's a paradigm shift in how browsers and servers communicate, enabling page load optimizations that were simply impossible before.
By the end of this page, you will understand: (1) Why HTTP/1.1's connection model limits performance, (2) How multiplexing interleaves multiple streams on one connection, (3) The mechanics of frame scheduling and stream management, (4) How multiplexing eliminates head-of-line blocking at the HTTP layer, (5) The practical performance implications for modern web applications, and (6) Why TCP limitations still matter and how HTTP/3 addresses them.
The HTTP/1.1 Connection Problem
To appreciate multiplexing, we must first understand the problem it solves. HTTP/1.1's connection model creates fundamental constraints that limit web performance.
The Request-Response Serialization Problem:
HTTP/1.1 connections are inherently serial. Each connection can process one request-response pair at a time:
Connection 1: [Request A] → [Response A] → [Request B] → [Response B] → ...
While HTTP/1.1 introduced pipelining to send multiple requests without waiting for responses, it had a fatal flaw: responses must return in the same order as requests. If Response A takes 500ms and Response B takes 10ms, Response B waits behind Response A—even though it's ready immediately.
This is head-of-line (HOL) blocking at the HTTP layer.
| Problem | Description | Performance Impact |
|---|---|---|
| Serial Processing | One request-response pair per connection at a time | Connection utilization drops during request/response wait times |
| Head-of-Line Blocking | Pipelined responses must return in order | Fast responses blocked by slow ones; unpredictable latency |
| Connection Limits | Browsers limit 6-8 connections per domain | Limits parallelism; creates artificial bottlenecks |
| Connection Overhead | Each connection requires TCP handshake + TLS setup | 3+ RTTs overhead per new connection; expensive for many small resources |
| Header Redundancy | Every request repeats full headers | Kilobytes of duplicate data per page load |
The Workaround Era:
Web developers invented numerous workarounds to mitigate HTTP/1.1's limitations:
-
Domain Sharding: Split resources across multiple domains (cdn1.example.com, cdn2.example.com) to bypass per-domain connection limits. Adds DNS lookups and TLS handshakes.
-
Resource Bundling: Concatenate JavaScript and CSS files to reduce request count. Breaks caching granularity; change one line, re-download megabytes.
-
Image Sprites: Combine many images into one sprite sheet. Complex maintenance; downloads unused images.
-
Inlining: Embed CSS, JS, or images directly in HTML. Eliminates caching; bloats HTML.
-
Lazy Loading: Defer non-critical resources. Adds complexity; may delay user experience.
These workarounds added development complexity, hurt caching efficiency, and still couldn't fully solve the fundamental problem. HTTP/2 multiplexing makes most of them unnecessary.
A typical web page loads 70+ resources. With 6 connections and serial processing, loading these resources requires 12+ round trips. Each additional RTT adds latency proportional to network distance—50ms on a fast connection, 200ms+ on mobile networks. The cumulative effect: pages that could load in 500ms take 2-3 seconds, purely due to HTTP/1.1 protocol limitations.
Multiplexing Fundamentals
HTTP/2 multiplexing allows multiple independent request-response exchanges—called streams—to share a single TCP connection. Frames from different streams interleave freely, with the Stream Identifier in each frame header enabling the receiver to reassemble the correct stream.
The Core Concept:
Instead of:
Connection 1: [Req A]────────[Resp A]────────[Req B]────────[Resp B]
Connection 2: [Req C]────────[Resp C]────────[Req D]────────[Resp D]
HTTP/2 does:
Connection: [A][C][B][A][D][C][B][A][D][C][B][A]...
│ │ │ │ │ │ │ │ │ │ │ └── Frames interleaved
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──── All streams on ONE connection
Each letter represents a frame. Frames from streams A, B, C, and D arrive interleaved, but the receiver reconstructs each logical stream independently.
Why This Works:
Multiplexing is possible because of HTTP/2's binary framing:
- Stream Identifiers: Every frame includes a 31-bit stream ID, enabling demultiplexing
- Fixed Frame Boundaries: The receiver knows exactly where each frame ends
- Independent Reassembly: Frames are collected per-stream and reassembled independently
- No Ordering Requirement: Unlike HTTP/1.1 pipelining, responses can complete in any order
When Stream 3's response finishes before Stream 1, it's delivered immediately. The client processes each stream as it completes—no waiting for earlier requests.
A stream is a logical, bidirectional sequence of frames for a single request-response. Streams exist only at the HTTP layer—the underlying TCP connection sees only a sequence of bytes. This key insight explains why HTTP/2 can carry hundreds of concurrent 'conversations' over a single socket.
Stream Mechanics
Understanding stream mechanics is essential for grasping how multiplexing achieves its performance benefits.
Stream Identification:
Streams use odd or even identifiers based on who initiated them:
| Initiator | Stream IDs | Purpose |
|---|---|---|
| Client | 1, 3, 5, 7, ... | Regular requests |
| Server | 2, 4, 6, 8, ... | Server push (covered later) |
| Connection | 0 | SETTINGS, PING, GOAWAY |
This prevents ID conflicts—client and server can create streams independently without coordination.
Stream States:
Each stream progresses through states as frames are exchanged:
idle → open → half-closed (local) → closed
or
idle → open → half-closed (remote) → closed
State transitions are triggered by specific frame types and flags:
- HEADERS with END_STREAM: Opens and immediately half-closes (request with no body)
- DATA with END_STREAM: Signals the sender has no more data
- RST_STREAM: Immediately terminates the stream (error or cancellation)
Stream Creation:
A client creates a stream by sending a HEADERS frame with a new, unused odd stream ID. The stream enters the 'open' state when the HEADERS is received. No explicit "create stream" message exists—sending HEADERS implicitly creates the stream.
| State | Legal Actions (Client) | Legal Actions (Server) | Typical Scenario |
|---|---|---|---|
| idle | Send HEADERS | Send PUSH_PROMISE | Stream not yet used |
| open | Send DATA, HEADERS continuation | Send DATA, HEADERS | Active request-response exchange |
| half-closed (local) | Receive DATA, HEADERS | Send DATA, HEADERS | Request sent, awaiting response |
| half-closed (remote) | Send DATA, HEADERS | Receive DATA | Response body being sent |
| closed | None | None | Stream complete; ID retired |
Concurrent Stream Management:
The MAX_CONCURRENT_STREAMS setting limits how many streams can be in the 'open' or 'half-closed' state simultaneously. This prevents resource exhaustion:
- Client perspective: Limits how many requests can be in flight at once
- Server perspective: Limits memory and CPU allocated to concurrent processing
Typical values: 100-1000 concurrent streams. Compare to HTTP/1.1's 6 connections—HTTP/2 provides 15-150x more parallelism on a single socket!
Once a stream closes, its ID is permanently retired. This prevents ambiguity when delayed frames arrive—a frame for stream 7 either belongs to an active stream or is an error. After exhausting 2^31-1 odd numbers (~1 billion streams), the client must establish a new connection. At 1000 streams/second, this takes 12+ days—not a practical concern.
Frame Interleaving and Scheduling
Multiplexing enables frame interleaving, but how frames are actually scheduled determines real-world performance. Both senders (client and server) must decide which stream's frames to transmit next.
The Scheduling Challenge:
Consider a server with three responses ready to send:
- Stream 1: 100KB HTML document (critical for rendering)
- Stream 3: 2MB image (can load progressively)
- Stream 5: 50KB JavaScript (blocks rendering)
Naive round-robin scheduling sends frames in rotation:
[S1][S3][S5][S1][S3][S5][S1][S3][S5]...
But this delays critical resources. The JavaScript (Stream 5) and HTML (Stream 1) should arrive before the large image starts consuming bandwidth.
Priority-Based Scheduling:
HTTP/2 includes a prioritization mechanism (explored in detail later in this module) that guides frame scheduling:
- Streams declare dependencies on other streams
- Streams have relative weights affecting resource allocation
- Senders should respect priorities when scheduling frames
With proper priorities:
[S1][S1][S5][S1][S5][S1][S5][S5][S5][S3][S3][S3]...
HTML first ─┘ └── JS next ───┘ └── Image last
The HTML and JavaScript complete while the image is just starting. The browser can begin rendering much earlier.
- •Fairness vs. Priority: Pure priority starves low-priority streams; some fairness prevents starvation
- •Frame Size Impact: Larger frames reduce overhead but increase blocking; 16KB default balances these
- •Buffer Availability: Flow control windows limit how much data can be sent per stream
- •TCP Considerations: TCP congestion control affects overall throughput regardless of HTTP/2 scheduling
- •Server-Side Variability: Different servers implement different scheduling algorithms; behavior varies
Practical Implications:
In practice, browsers signal priorities through stream dependencies:
Root (0)
│
┌───────┴───────┐
CSS (3) JS (5)
w=256 w=32
│
HTML (1)
w=256
This dependency tree tells the server: "HTML depends on CSS (CSS must arrive first for proper styling), and CSS is more important than JS (when bandwidth-constrained, prefer CSS frames)."
However, priority is advisory—servers may ignore it, and many implementations use simplified scheduling. Understanding your CDN's behavior is important for optimization.
HTTP/2's original priority mechanism proved complex and inconsistently implemented. HTTP/3 and the 'Extensible Priority' proposal simplify this with urgency levels and incremental hints. Many HTTP/2 implementations now support these newer semantics via the Priority header field.
Head-of-Line Blocking: Solved... Sort Of
HTTP/2 multiplexing completely eliminates HTTP-layer head-of-line blocking. If Stream 1's response is slow, Streams 3, 5, and 7 can complete and be processed immediately. This is a massive improvement over HTTP/1.1 pipelining.
The Victory:
HTTP/1.1 Pipelining:
Request: [A][B][C] ────────────────→
Response: [A........................][B][C]
└── B and C blocked by A ──┘
HTTP/2 Multiplexing:
Request: [A][B][C] ────────────────→
Response: [A chunk][B chunk][C][B][C][A chunk][A]
└── All complete as ready, no blocking ──┘
For HTTP layer interactions, this is exactly what we wanted.
However, HTTP/2 still runs over TCP, and TCP has its own head-of-line blocking. If a single packet is lost, TCP holds all subsequent data until that packet is retransmitted—even if the lost packet contained data for Stream 1 and the waiting packets are for Stream 5. At the TCP layer, there's only one byte stream.
TCP Head-of-Line Blocking Explained:
Consider this scenario:
- Server sends frames: [S1 pkt1][S3 pkt1][S5 pkt1][S1 pkt2][S3 pkt2]
- Packet 2 (S3 pkt1) is lost in transmission
- Packets 3, 4, 5 arrive at the client
- TCP cannot deliver packets 3, 4, 5 to HTTP/2 layer
- Application waits until packet 2 is retransmitted and received
- Only then are packets 2, 3, 4, 5 delivered together
From HTTP/2's perspective, everything stops. Stream 5's data was ready, but TCP won't release it because Stream 3's earlier packet was lost. The multiplexing benefit is negated by TCP's ordering guarantee.
The Impact:
On reliable networks (wired, data centers), TCP packet loss is rare (~0.01%), and this problem is minimal. On unreliable networks (cellular, weak WiFi), loss rates of 1-5% mean TCP HOL blocking occurs frequently.
Studies have shown that on high-loss networks, HTTP/1.1 with multiple connections can actually outperform HTTP/2 because lost packets affect only one connection's streams, not all of them.
This is the fundamental motivation for HTTP/3, which uses QUIC (UDP-based) to enable per-stream packet loss recovery. More on this in the HTTP/3 section.
Despite TCP limitations, HTTP/2 multiplexing provides substantial benefits for most users: reduced connection overhead, better server resource utilization, header compression sharing, and simplified infrastructure (no domain sharding). The TCP HOL blocking issue primarily affects high-latency, high-loss networks—a minority of traffic for most applications.
Practical Performance Benefits
Multiplexing delivers measurable performance improvements across multiple dimensions. Let's quantify the benefits:
Connection Overhead Reduction:
HTTP/1.1 requires multiple connections. Each connection incurs:
- DNS lookup (if new domain): 50-150ms
- TCP handshake: 1 RTT (~30-100ms)
- TLS handshake: 1-2 RTTs (~60-200ms)
- TCP slow start: Reduced initial throughput
With HTTP/2, a page loading 70 resources over 6 domains pays this cost once per domain instead of 6+ times. Typical savings: 200-500ms on initial page load.
| Metric | HTTP/1.1 (6 connections/domain) | HTTP/2 (1 connection/domain) | Improvement |
|---|---|---|---|
| TCP Handshakes | ~36 (6 domains × 6 conn) | ~6 (1 per domain) | 6x reduction |
| TLS Handshakes | ~36 | ~6 | 6x reduction |
| Connection Overhead | ~7.2 seconds cumulative | ~1.2 seconds cumulative | ~6 seconds saved |
| Server Memory per Client | 36 TCP buffers | 6 TCP buffers | 6x reduction |
| Concurrent Request Limit | ~36 (6 domains × 6) | 600+ (100/domain × 6) | 15x+ increase |
Latency Improvements:
Multiplexing particularly shines for latency-sensitive metrics:
- Time to First Byte (TTFB): Similar to HTTP/1.1 for first request; better for subsequent resources
- First Contentful Paint (FCP): Critical CSS/JS delivered without waiting for parallel connections
- Largest Contentful Paint (LCP): Hero images no longer compete with blocked connections
- Time to Interactive (TTI): JavaScript bundles delivered without HOL blocking
Real-world studies show 10-40% improvements in these metrics depending on page structure and network conditions.
- •Eliminates Connection Blocking: No waiting for available connections; all requests start immediately
- •Maximizes Bandwidth Utilization: Single connection achieves full throughput faster (no slow start multiplication)
- •Reduces Server Load: Fewer TCP states to maintain per client; better scalability
- •Enables Asset Unbundling: Many small files no longer penalized; granular caching possible
- •Simplifies CDN Configuration: No domain sharding needed; reduced DNS complexity
- •Improves Mobile Performance: Fewer connections mean less radio wakeup and better battery life
With HTTP/2, many HTTP/1.1 optimizations become anti-patterns. Bundling multiple scripts into one giant file hurts caching—change one module, invalidate entire bundle. HTTP/2's efficient multiplexing means shipping many small, independently cacheable modules is often superior. Tree-shaking and ESM imports align perfectly with this approach.
Server Resource Management
Multiplexing changes how servers allocate and manage resources. Understanding these changes is essential for operating HTTP/2 infrastructure at scale.
Connection Pooling:
HTTP/1.1 servers maintain connection pools—each pool connection handles one request at a time. With 10,000 concurrent users sending 6 parallel requests each, the server manages 60,000 connections.
HTTP/2 servers serve the same users with ~10,000 connections (one per user), each carrying multiple streams. This dramatically reduces:
- Socket descriptors: OS-level limits (ulimit) are less likely to be reached
- Memory per connection: TCP buffers are per-connection, not per-request
- Context switching: Fewer connections mean less scheduler overhead
Stream Concurrency Control:
The MAX_CONCURRENT_STREAMS setting lets servers control resource allocation:
SETTINGS_MAX_CONCURRENT_STREAMS = 100
This tells clients: "Limit active streams to 100 per connection." Servers set this based on:
- Available memory per connection
- Expected request processing time
- Desired fairness across clients
If a client exceeds this limit, the server responds with REFUSED_STREAM or closes the connection. Clients must queue excess requests locally.
| Resource | HTTP/1.1 Scaling | HTTP/2 Scaling | Notes |
|---|---|---|---|
| Memory | Per connection (~50KB TCP + TLS) | Per connection + per stream (~10KB/stream) | More memory efficient overall |
| CPU | Parallel request processing | Parallel stream processing + frame multiplexing | Similar; H2 adds framing overhead |
| Connections | 6+ per client | 1 per client (typically) | Major reduction |
| Backend Latency | Blocks connection | Blocks stream only | Better utilization |
| Timeouts | Per connection | Per connection + per stream | More granular control |
HTTP/2's long-lived, high-capacity connections change load balancing dynamics. A single connection might carry 100+ requests, so connection-based load balancing is coarser-grained. Some load balancers now offer request-level (L7) distribution for HTTP/2, but at the cost of terminating connections at the balancer.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.