HTTP Cheatsheet
HTTP/2 and HTTP/3
Use this HTTP reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Version comparison
| HTTP/1.1 (1997) | HTTP/2 (2015) | HTTP/3 (2022) | |
|---|---|---|---|
| Framing | text, CRLF delimited | binary frames | binary frames |
| Transport | TCP | TCP | QUIC over UDP |
| RFC | 9112 | 9113 | 9114 |
| Requests per connection | one at a time, in order | multiplexed streams | multiplexed streams |
| Head-of-line blocking | at the HTTP layer | at the TCP layer, on loss | none, per-stream loss recovery |
| Header compression | none | HPACK | QPACK |
| Header case | insensitive, mixed used | must be lowercase | must be lowercase |
| Server push | no | yes, removed in practice | no |
| Priority | none | dependency tree, then RFC 9218 | RFC 9218 extensible priorities |
| Encryption | optional | optional in spec, TLS in every browser | built in, always |
| Handshake to first byte | TCP 1 RTT + TLS 1-2 RTT | same | 1 RTT, 0 RTT on resumption |
| Connection migration | no, a new IP breaks it | no | yes, by connection ID |
| Discovery | default | ALPN h2, or Upgrade for h2c | Alt-Svc, or DNS HTTPS record |
HTTP/2 multiplexing and frames
One TCP connection carries many independent streams, each with an odd client-initiated id, and each stream is a sequence of frames.
one TCP + TLS connection ┌──────────────────────────────────────────────────────┐ │ stream 1 HEADERS ──── DATA ──── DATA ── END_STREAM │ /index.html │ stream 3 HEADERS ── DATA ── END_STREAM │ /app.css │ stream 5 HEADERS ─────── DATA ──────── END_STREAM │ /app.js └──────────────────────────────────────────────────────┘ frames from different streams interleave freely
| Frame type | Carries |
|---|---|
HEADERS | request or response header block, HPACK compressed |
CONTINUATION | header block overflow |
DATA | body bytes |
SETTINGS | connection parameters, exchanged at start |
WINDOW_UPDATE | flow control credit, per stream and per connection |
RST_STREAM | cancel one stream without touching the connection |
PRIORITY | dependency and weight, deprecated in favor of the header |
PING | liveness and RTT measurement |
GOAWAY | graceful connection shutdown, names the last accepted stream |
PUSH_PROMISE | server push, effectively dead |
Because one request can be cancelled with RST_STREAM while others continue, an
aborted fetch no longer wastes a whole connection. That same cheapness was the
basis of the HTTP/2 Rapid Reset denial-of-service class, which servers now
mitigate by rate-limiting resets.
Pseudo-headers and what disappears
HTTP/2 and /3 replace the request line with four pseudo-headers, all lowercase and all before the regular fields.
| Pseudo-header | HTTP/1.1 equivalent |
|---|---|
:method | the method token |
:scheme | http or https |
:authority | the Host header |
:path | the request target |
:status | the status code, response only |
Removed or forbidden: Connection, Keep-Alive, Transfer-Encoding,
Upgrade, and Proxy-Connection are all connection-specific and are protocol
errors in HTTP/2 and /3. There is no reason phrase, so HTTP/2 404 has no
"Not Found" text on the wire.
HPACK and QPACK compress headers with a static table of 61 common name-value pairs plus a dynamic table shared across the connection, which is why repeated cookies and user agents cost almost nothing after the first request. HPACK's shared dynamic table is what made the CRIME-style HEIST concerns apply to headers, and QPACK reworks it to avoid QUIC head-of-line issues.
What HTTP/2 changed about optimization
Practices that were correct for HTTP/1.1 became counterproductive.
| Old practice | Why it existed | Under HTTP/2 and /3 |
|---|---|---|
| Domain sharding | 6 connections per origin limit | harmful, defeats multiplexing and one warm connection |
| Concatenating all JS into one bundle | fewer requests | less important, and it wrecks cache granularity |
| CSS sprites | fewer requests | unnecessary |
| Inlining assets as data URIs | fewer requests | usually harmful, they cannot be cached separately |
| Cookie-free asset domains | header overhead per request | mostly moot, HPACK compresses them |
What still matters at every version: fewer bytes, correct caching, content-hashed
filenames, and preload hints. And one thing gets worse, not better: because
everything shares one TCP connection, a single packet loss stalls every stream
until TCP recovers. That is precisely the problem HTTP/3 exists to solve.
QUIC and HTTP/3
QUIC is a transport built on UDP that folds in what TCP plus TLS did separately.
| Property | Detail |
|---|---|
| Transport | UDP, port 443 by convention |
| Encryption | TLS 1.3 integrated, not layered, and there is no unencrypted mode |
| Handshake | 1 RTT, or 0 RTT with a resumption ticket |
| Streams | independent, so loss on one does not block another |
| Loss recovery | per stream, with packet-number-based ack ranges |
| Connection ID | survives an IP or port change, so Wi-Fi to cellular does not break |
| Congestion control | pluggable, CUBIC or BBR in userspace |
| Head-of-line blocking | eliminated at the transport layer |
| Ossification resistance | headers encrypted, so middleboxes cannot meddle |
Because QUIC lives in userspace over UDP, it deploys without kernel changes, and it fails where UDP 443 is blocked. Clients handle that by racing QUIC against TCP and falling back, so a blocked network shows up as a latency cost, not an error.
Negotiation and discovery
HTTP/2 over TLS ALPN offers "h2", server selects it in the handshake HTTP/2 cleartext h2c via Upgrade, no browser supports it HTTP/3 advertised on an h2 or h1 response: HTTP/1.1 200 OK Alt-Svc: h3=":443"; ma=86400 or resolved directly, skipping the first round trip: $ dig HTTPS example.com +short 1 . alpn="h3,h2" ipv4hint=203.0.113.9
A browser's first request to a new origin cannot be HTTP/3, because it does
not yet know QUIC is available. It learns from Alt-Svc or from the DNS HTTPS
record and upgrades subsequent connections.
Verifying which version you got
curl -sI --http2 https://example.com | head -1 # HTTP/2 200 curl -sI --http3 https://example.com | head -1 # HTTP/3 200 curl -s -o /dev/null -w '%{http_version}\n' https://example.com curl --http1.1 -sI https://example.com | head -1 # force downgrade curl -sI https://example.com | grep -i alt-svc # confirm ALPN selection in the handshake itself openssl s_client -connect example.com:443 -servername example.com \ -alpn h2 </dev/null 2>/dev/null | grep -i 'ALPN protocol' nghttp -nv https://example.com # frame-level trace dig HTTPS example.com +short # HTTPS/SVCB record
| Symptom | Likely cause |
|---|---|
%{http_version} is 1.1 on a modern server | ALPN not offering h2, or a proxy downgrading |
| HTTP/2 works, HTTP/3 never negotiates | no Alt-Svc, or UDP 443 blocked on the path |
| Works on 1.1, breaks on 2 | a forbidden connection header, or mixed-case header names |
| Everything stalls together on a lossy link | TCP head-of-line blocking, the case for HTTP/3 |
| 421 Misdirected Request | connection coalescing sent the request to the wrong origin |