Field note / fundamentals

publishedupdated 2026-05-01#http#networking#quic#performance#interview

HTTP 1.0 → HTTP 1.1 → HTTP 2.0 → HTTP 3.0

How each HTTP version solved the previous generation's bottlenecks — persistent connections, multiplexing, QUIC, and HOL blocking.

Overview

Each HTTP version was born to fix a specific bottleneck in the previous one. Understanding the progression is a standard system design interview topic — you'll be asked "why is HTTP/2 better than HTTP/1.1" or "when would you use HTTP/3?"

VersionYearKey Fix
HTTP/1.01996Baseline — one request per TCP connection
HTTP/1.11997Persistent connections, pipelining
HTTP/2.02015Multiplexing, header compression, server push
HTTP/3.02022QUIC over UDP, removes transport-layer HOL

Architecture

HTTP/1.0  ─── one TCP connection per request ──────────────────
  Client                     Server
    │── TCP handshake ──────────▶│
    │── GET /index.html ────────▶│
    │◀─ 200 OK ─────────────────│
    │── TCP close ──────────────▶│
    │                            │
    │── TCP handshake (new!) ───▶│   ← NEW connection for every request
    │── GET /style.css ─────────▶│
    │◀─ 200 OK ─────────────────│

─────────────────────────────────────────────────────────────────
HTTP/1.1  ─── persistent connection (keep-alive) ───────────────
  Client                     Server
    │── TCP handshake ──────────▶│
    │── GET /index.html ────────▶│
    │◀─ 200 OK ─────────────────│
    │── GET /style.css ─────────▶│   ← same connection reused
    │◀─ 200 OK ─────────────────│
    │── GET /app.js ────────────▶│   ← but still SEQUENTIAL
    │◀─ 200 OK ─────────────────│   ← HOL blocking if /style.css is slow

─────────────────────────────────────────────────────────────────
HTTP/2.0  ─── multiplexing over single connection ───────────────
  Client                     Server
    │── TCP handshake ──────────▶│
    │                            │
    │ Stream 1: GET /index.html ─▶│
    │ Stream 2: GET /style.css ──▶│   ← all in parallel
    │ Stream 3: GET /app.js ─────▶│   ← on ONE TCP connection
    │                            │
    │◀─ Stream 2: 200 OK ────────│   ← responses out of order
    │◀─ Stream 1: 200 OK ────────│
    │◀─ Stream 3: 200 OK ────────│
    │                            │
    NOTE: if a TCP packet is lost, ALL streams stall ← TCP HOL

─────────────────────────────────────────────────────────────────
HTTP/3.0  ─── QUIC over UDP (no TCP) ────────────────────────────
  Client                     Server
    │── QUIC handshake (1-RTT) ─▶│   ← TLS 1.3 built in
    │                            │
    │ QUIC Stream 1: /index ─────▶│
    │ QUIC Stream 2: /style ─────▶│   ← independent streams
    │ QUIC Stream 3: /app ───────▶│
    │                            │
    │◀─ Stream 2 response ───────│
    │◀─ (Stream 1 packet lost)   │   ← only Stream 1 retransmits
    │◀─ Stream 3 response ───────│   ← Stream 3 NOT blocked!

Technical Implementation

HTTP/1.1 HOL Problem (Code Illustration)

// HTTP/1.1: browsers open ~6 parallel connections per domain
// but each connection is sequential → head-of-line blocking

// If /api/slow takes 3s, /api/fast must WAIT behind it in the same connection
// Workaround: domain sharding (cdn1.example.com, cdn2.example.com)
// → anti-pattern in HTTP/2 (hurts because it prevents connection reuse)

HTTP/2 Server Push

// Node.js HTTP/2 server push — proactively send CSS with HTML
const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({ key, cert });

server.on('stream', (stream, headers) => {
  if (headers[':path'] === '/') {
    // Push CSS before client asks for it
    stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => {
      pushStream.respondWithFile('./style.css');
    });
    stream.respondWithFile('./index.html');
  }
});

HTTP/3 Connection (Node.js via quic package)

// HTTP/3 uses QUIC — built into browsers, enabled via Alt-Svc header
// Server signals HTTP/3 support:
res.setHeader('Alt-Svc', 'h3=":443"; ma=86400');
// Browser connects via QUIC on next request

// Key config: TLS 1.3 required, UDP port 443

Enabling HTTP/2 in nginx

server {
    listen 443 ssl http2;  # ← add 'http2' flag
    ssl_certificate     /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # HTTP/2 push
    http2_push /style.css;
    http2_push /app.js;
}

Interview Preparation

Q: What is HOL (Head-of-Line) blocking and which HTTP versions have it?

HOL blocking: a slow request blocks all subsequent requests in the same queue.

  • HTTP/1.1: one request at a time per connection — slow response blocks everything behind it on that connection.
  • HTTP/2: eliminated at the application layer (streams are independent), but a lost TCP packet blocks all streams while waiting for retransmission — TCP-layer HOL.
  • HTTP/3: eliminated at both layers. QUIC streams are independent at the transport level — a lost packet only stalls that specific stream.

Q: Why does HTTP/2 use a single TCP connection instead of multiple?

Multiple connections waste resources (3-way handshake, TLS negotiation, separate congestion windows). HTTP/2 multiplexes all streams over one connection — more efficient use of the TCP connection's congestion window, one TLS session, and the server can prioritize stream delivery.

Q: When should you use HTTP/3?

When users are on high-packet-loss networks (mobile, WiFi, cellular). HTTP/3's QUIC handles packet loss per-stream, so a 1% packet loss rate that would stall HTTP/2 streams barely affects HTTP/3. Also useful for applications where connection setup latency matters — QUIC combines the transport and TLS handshakes into 1-RTT (vs TCP's 3-way + TLS = 2-3 RTTs).

Q: What is QUIC?

QUIC (Quick UDP Internet Connections) is a transport protocol built on UDP. It reimplements TCP's reliability features (acknowledgment, retransmission, flow control) at the application layer, plus adds independent stream multiplexing and 0-RTT connection resumption. Developed by Google, standardized as RFC 9000.


Learning Resources