Skip to main content
GM
  • Skills
  • Projects
  • Experience
  • Education
  • Contact
  • Blog
Back to all posts
HTTP Concurrency: The Misconceptions I Had to Unlearn

HTTP Concurrency: The Misconceptions I Had to Unlearn

November 3, 2026
·
11 min read
httpnetworkingtcphttp2quicweb-architecture

A breakdown of networking mental models: unlearning the packet ACK fallacy, the 6-connection Pringles hack, HTTP/2 blast radius, and QUIC stream mechanics


HTTP Concurrency: The Misconceptions I Had to Unlearn

The quickest way to get confused by HTTP is to treat the network as one layer.

When you blur HTTP requests, TCP connections, TLS handshakes, packets, and acknowledgements into a single bucket, every architectural shift between HTTP/1.1, HTTP/2, and HTTP/3 sounds like arbitrary churn. The explanations you run into make it worse: they claim HTTP/1.1 is slow because it waits for packet ACKs, that HTTP is stateless so it rebuilds connections, that HTTP/2 makes TCP unordered, or that QUIC is just UDP made faster.

None of those are true. The actual evolution is a sequence of mental models collapsing when pushed against the layer underneath them.

The stateless scope trap

"HTTP is stateless" gets repeated until it sounds like the connection has amnesia.

Stateless is an application-layer rule. It means the server does not retain conversational context between request A and request B. It does not mean the operating system tears down the transport stack.

A single persistent TCP connection with TLS can carry dozens of sequential HTTP requests:

TCP handshake (SYN, SYN-ACK, ACK)
  ↓
TLS handshake (key exchange, certificates)
  ↓
HTTP Request 1 → HTTP Response 1
HTTP Request 2 → HTTP Response 2
HTTP Request 3 → HTTP Response 3
  ↓
Connection closes (FIN)

The TCP connection holds state. The TLS session holds state. The HTTP requests running across that connection do not.

TCP handshakes do not happen per request.⁠‌‍‌‌‍‌​‍‍‌‍‍​‌‌​‌‌​‌​​​‍‌‌‍​‌‍‌‍‍‌‍​‌‌‌‌​‍‌​‍‌‍‌‌‌‍‍‌‍​‌‍‌​‍‌‌‌​‌‍‍​‌‍‌‌​‍‌‌‍‌‌‍‍‌‌​‌‍​‌‍‌‍‍‌‍​‌‍‌‌‌​​‌‌​‌‍‍‌‌‍‌‍‍‌​​‍‌‌‍‍‌​‍‌‌‍‍​‌‍​‌‌‍‌​​‍‌‌‌​‌‍​‍‌‌‌‌‌‍‍‌‍​‌‍‌‌‌‍​‌‌​‍‌‍‍​‍‍​​‌​‌​‍‌​​​‌‍​‌​‌​​​‌‌​‌​‍‍‌‍​‌​​​​‌​‌‍‌‍‌‍​‍‌​‌​​‌‍‌‍​‌​‌​‌‌‌‍‌​​‌‍​‍​​‌​‌‌⁠ A persistent HTTP/1.1 connection sets up the pipe once and keeps it alive. Rebuilding the TCP and TLS handshake on every request was an early HTTP/1.0 habit that persistent connections eliminated decades ago.

The packet ACK fallacy and the Pringles can

The standard explanation for why HTTP/1.1 feels slow is that it has to wait for an ACK before sending the next packet.

That mixes up the HTTP application layer with TCP packet mechanics.

TCP does not send a segment, pause, wait for an acknowledgement, and only then send the next segment. TCP uses sliding windows. It keeps dozens of segments in flight simultaneously while sequence numbers, ACKs, flow control, and congestion windows adjust in the background.

HTTP/1.1 stalled on request-level sequencing, while packet ACKs ran unhindered in the background:

Request A
   ↓
Response A
   ↓
Request B
   ↓
Response B

HTTP/1.1 connections enforce strict FIFO response ordering. In standard practice, clients send Request A, wait for Response A to finish arriving, and only then send Request B. Even with HTTP/1.1 pipelining, where a client dispatches requests back-to-back without waiting, the server must still return responses in exact arrival order. If Response A stalls on the server, everything behind it queues up.

The mental model that finally cleared this up is a Pringles can.

Imagine chips stacked inside a narrow can. Chip A sits at the rim, directly on top of B, C, D, and E. If Chip A gets stuck at the opening, you cannot reach Chip B. It does not matter that an electronic scoreboard on the wall (TCP ACKs) is updating without delay. The obstruction is physical: the object in front blocks the objects behind it.

That is HTTP/1.1 head-of-line blocking. The blocker is the response still occupying the wire. Network acknowledgements run on their own clock.

The browser workaround: six cans

HTTP/1.1 had concurrency. It just bought it by opening multiple connections.

Browsers faced with downloading HTML, CSS, JavaScript, fonts, and dozens of images did not wait for a single connection to chew through them sequentially. They opened up to six separate TCP connections to the same host:

TCP 1 → HTML
TCP 2 → CSS
TCP 3 → JavaScript
TCP 4 → Image A
TCP 5 → Image B
TCP 6 → Font

If the response on TCP 1 took two seconds to compute, TCP 2 through 6 kept downloading assets in parallel.

There was also a misconception that HTTP/1.1 could not tell which bytes belonged to which asset.⁠‌‍‌‌‍‌​‍‍‌‍‍​‌‌​‌‌​‌​​​‍‌‌‍​‌‍‌‍‍‌‍​‌‌‌‌​‍‌​‍‌‍‌‌‌‍‍‌‍​‌‍‌​‍‌‌‌​‌‍‍​‌‍‌‌​‍‌‌‍‌‌‍‍‌‌​‌‍​‌‍‌‍‍‌‍​‌‍‌‌‌​​‌‌​‌‍‍‌‌‍‌‍‍‌​​‍‌‌‍‍‌​‍‌‌‍‍​‌‍​‌‌‍‌​​‍‌‌‌​‌‍​‍‌‌‌‌‌‍‍‌‍​‌‍‌‌‌‍​‌‌​‍‌‍‍​‍‍​​‌​‌​‍‌​​​‌‍​‌​‌​​​‌‌​‌​‍‍‌‍​‌​​​​‌​‌‍‌‍‌‍​‍‌​‌​​‌‍‌‍​‌​‌​‌‌‌‍‌​​‌‍​‍​​‌​‌‌⁠ It could. Headers like Content-Length and chunked transfer framing explicitly marked response boundaries. The protocol could delimit resources cleanly; it simply could not interleave them over the same connection.

Opening six TCP connections was the only reliable workaround. It solved the concurrency problem by brute force: if one can jams, open five more cans.

HTTP/2 does not make TCP unordered

Opening six TCP connections per domain works for simple pages, but modern web applications load hundreds of small assets. Six connections mean six separate TCP three-way handshakes, six TLS negotiations, and six independent congestion windows competing for bandwidth.

HTTP/2 changed the model: one TCP connection carrying multiple logical streams.

ONE TCP CONNECTION
       │
 ┌─────┼─────┐
 ↓     ↓     ↓
Stream A  Stream B  Stream C

The common confusion here is assuming HTTP/2 tells TCP to deliver packets out of order.

HTTP/2 does not touch TCP ordering. TCP still guarantees one strictly ordered byte stream from sender to receiver.

HTTP/2 operates entirely above TCP. It splits requests and responses into small binary frames, prefixes each frame with a Stream ID, and interleaves those frames into TCP's single ordered stream. The receiver inspects the Stream ID on each frame and reassembles the streams independently:

[Frame A1] [Frame B1] [Frame C1] [Frame A2] [Frame C2] [Frame B2]

HTTP/2 solved concurrency by adding stream identity to frames, leaving TCP ordering untouched.

The blast radius problem

Collapsing six TCP connections into one created an unexpected consequence.

In HTTP/1.1, if a packet dropped on connection 2 while fetching CSS, connection 1 (HTML) and connection 3 (JavaScript) kept moving. The failure stayed isolated to its own pipe.

In HTTP/2, all logical streams share one TCP connection. If a single TCP segment containing a slice of Stream B drops, TCP halts delivery of every segment that arrived after it. TCP promised the application an unbroken, ordered sequence of bytes. It will not deliver frames for Stream A or Stream C out of its kernel buffer until the missing segment for Stream B is retransmitted and acknowledged.

Rendering diagram...
flowchart TD
    subgraph HTTP2["HTTP/2: One TCP Connection"]
        direction LR
        P1["Frame A1 (OK)"] --> P2["Frame B1 (LOST)"] --> P3["Frame C1 (HELD)"] --> P4["Frame A2 (HELD)"]
    end

TCP did not get worse. TCP behaved exactly as it always had.

The difference was the blast radius.⁠‌‍‌‌‍‌​‍‍‌‍‍​‌‌​‌‌​‌​​​‍‌‌‍​‌‍‌‍‍‌‍​‌‌‌‌​‍‌​‍‌‍‌‌‌‍‍‌‍​‌‍‌​‍‌‌‌​‌‍‍​‌‍‌‌​‍‌‌‍‌‌‍‍‌‌​‌‍​‌‍‌‍‍‌‍​‌‍‌‌‌​​‌‌​‌‍‍‌‌‍‌‍‍‌​​‍‌‌‍‍‌​‍‌‌‍‍​‌‍​‌‌‍‌​​‍‌‌‌​‌‍​‍‌‌‌‌‌‍‍‌‍​‌‍‌‌‌‍​‌‌​‍‌‍‍​‍‍​​‌​‌​‍‌​​​‌‍​‌​‌​​​‌‌​‌​‍‍‌‍​‌​​​​‌​‌‍‌‍‌‍​‍‌​‌​​‌‍‌‍​‌​‌​‌‌‌‍‌​​‌‍​‍​​‌​‌‌⁠ By multiplexing every stream into a single TCP connection, HTTP/2 turned TCP's core feature, strict ordered delivery, into a bottleneck for unrelated streams. On a lossy connection like mobile Wi-Fi or cellular networks, HTTP/2 can perform worse than HTTP/1.1 with multiple connections.

QUIC is not UDP made faster

HTTP/3 fixes TCP head-of-line blocking by replacing TCP entirely with QUIC, running over UDP.

HTTP/3
  ↓
QUIC
  ↓
UDP
  ↓
IP

The immediate misconception is that UDP is used because it is faster, or because modern networks are reliable enough to skip verification.

UDP provides virtually nothing. It gives you 16-bit port numbers and an optional checksum. It has no retransmissions, no connection state, no flow control, and no congestion control. If you blast raw UDP packets across the internet, dropped packets stay dropped.

QUIC does not skip reliability. QUIC implements connection establishment, packet loss detection, retransmission, congestion control, and TLS 1.3 encryption directly in user space.

The reason QUIC runs over UDP is ossification. Over thirty years, middleboxes, routers, firewalls, and NAT devices across the global internet have baked TCP header expectations into silicon. If you attempt to roll out a brand-new transport protocol at the IP layer, middleboxes drop the packets because they do not recognize the protocol number. UDP was the only transport protocol widely deployed that middleboxes would pass through untouched.

UDP was an escape hatch against middlebox ossification, not a speed shortcut.

Stream isolation

The actual breakthrough in QUIC is moving stream awareness into the transport layer.

In HTTP/2, TCP knows only about a single sequence of bytes. It has no concept of HTTP streams.

In HTTP/3, QUIC itself manages the streams. Each stream is an independent byte-offset sequence carried inside stream frames. Packets belong to the connection, but the frames inside them carry data for specific streams:

Rendering diagram...
flowchart TD
    subgraph QUIC["HTTP/3: QUIC Transport"]
        direction TB
        subgraph SA["Stream A"]
            A1["Stream A [Bytes 0..1023] (Delivered)"]
        end
        subgraph SB["Stream B"]
            B1["Stream B [Bytes 0..1023] (Lost in Packet 42)"] -.-> B2["Retransmitted in Packet 45 (Delivered)"]
        end
        subgraph SC["Stream C"]
            C1["Stream C [Bytes 0..1023] (Delivered)"]
        end
    end

If a packet carrying data for Stream B drops, only Stream B pauses and waits for retransmission. Stream A and Stream C continue delivering their byte offsets to the application immediately. QUIC never retransmits old packets; it packages unacknowledged stream offsets into fresh packets with new connection sequence numbers.

QUIC also eliminates another redundant delay: connection setup. HTTP/1.1 and HTTP/2 require a TCP three-way handshake followed by a separate TLS handshake. QUIC folds the TLS 1.3 cryptographic handshake directly into its own connection establishment, dropping connection setup to a single round trip (or zero round trips on resumed connections).

Duplex communication was never QUIC's differentiator either; TCP was full-duplex from day one.⁠‌‍‌‌‍‌​‍‍‌‍‍​‌‌​‌‌​‌​​​‍‌‌‍​‌‍‌‍‍‌‍​‌‌‌‌​‍‌​‍‌‍‌‌‌‍‍‌‍​‌‍‌​‍‌‌‌​‌‍‍​‌‍‌‌​‍‌‌‍‌‌‍‍‌‌​‌‍​‌‍‌‍‍‌‍​‌‍‌‌‌​​‌‌​‌‍‍‌‌‍‌‍‍‌​​‍‌‌‍‍‌​‍‌‌‍‍​‌‍​‌‌‍‌​​‍‌‌‌​‌‍​‍‌‌‌‌‌‍‍‌‍​‌‍‌‌‌‍​‌‌​‍‌‍‍​‍‍​​‌​‌​‍‌​​​‌‍​‌​‌​​​‌‌​‌​‍‍‌‍​‌​​​​‌​‌‍‌‍‌‍​‍‌​‌​​‌‍‌‍​‌​‌​‌‌‌‍‌​​‌‍​‍​​‌​‌‌⁠ QUIC's defining contribution is transport-level stream isolation.

The shifting bottleneck

Each step from HTTP/1.1 to HTTP/3 responded to shifting workloads rather than flawed protocols.

HTTP/1.1 worked well for the web of the late 1990s, where fetching a document and an image or two was normal. When pages shifted to complex applications demanding dozens of assets, the single-exchange constraint forced browsers to burn system resources on parallel TCP connections.

HTTP/2 solved connection overhead by multiplexing streams over one TCP pipe, which exposed TCP's byte-ordering guarantee as a shared failure point.

QUIC moved stream management into the transport layer so packet loss stays isolated to the stream that lost data.

Every generation solved the bottleneck of its predecessor, only to run directly into the constraints of the layer beneath it.

Newsletter

Occasional notes on backend architecture, GenAI experiments, and building reliable software. No spam, ever.

Gourab MondalFull Stack Developer · Kolkata, India

Interested mainly in backend systems, GenAI applications, and building reliable developer tools.

Navigation

  • Home
  • Skills
  • Projects
  • Experience
  • Education
  • Contact

Featured Code

  • Payment Sys
  • Audio Saas Backend
  • Monolithic Cicd Pipeline
  • Secrets Manager
  • All Repositories →

Connect & Protocols

  • gourab.m099@gmail.com
  • Kolkata, India
  • llms.txt (Agent Feed)
  • Sitemap XML
© 2026 Gourab Mondal. All rights reserved.