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)
Framingtext, CRLF delimitedbinary framesbinary frames
TransportTCPTCPQUIC over UDP
RFC911291139114
Requests per connectionone at a time, in ordermultiplexed streamsmultiplexed streams
Head-of-line blockingat the HTTP layerat the TCP layer, on lossnone, per-stream loss recovery
Header compressionnoneHPACKQPACK
Header caseinsensitive, mixed usedmust be lowercasemust be lowercase
Server pushnoyes, removed in practiceno
Prioritynonedependency tree, then RFC 9218RFC 9218 extensible priorities
Encryptionoptionaloptional in spec, TLS in every browserbuilt in, always
Handshake to first byteTCP 1 RTT + TLS 1-2 RTTsame1 RTT, 0 RTT on resumption
Connection migrationno, a new IP breaks itnoyes, by connection ID
DiscoverydefaultALPN h2, or Upgrade for h2cAlt-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 typeCarries
HEADERSrequest or response header block, HPACK compressed
CONTINUATIONheader block overflow
DATAbody bytes
SETTINGSconnection parameters, exchanged at start
WINDOW_UPDATEflow control credit, per stream and per connection
RST_STREAMcancel one stream without touching the connection
PRIORITYdependency and weight, deprecated in favor of the header
PINGliveness and RTT measurement
GOAWAYgraceful connection shutdown, names the last accepted stream
PUSH_PROMISEserver 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-headerHTTP/1.1 equivalent
:methodthe method token
:schemehttp or https
:authoritythe Host header
:paththe request target
:statusthe 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 practiceWhy it existedUnder HTTP/2 and /3
Domain sharding6 connections per origin limitharmful, defeats multiplexing and one warm connection
Concatenating all JS into one bundlefewer requestsless important, and it wrecks cache granularity
CSS spritesfewer requestsunnecessary
Inlining assets as data URIsfewer requestsusually harmful, they cannot be cached separately
Cookie-free asset domainsheader overhead per requestmostly 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.

PropertyDetail
TransportUDP, port 443 by convention
EncryptionTLS 1.3 integrated, not layered, and there is no unencrypted mode
Handshake1 RTT, or 0 RTT with a resumption ticket
Streamsindependent, so loss on one does not block another
Loss recoveryper stream, with packet-number-based ack ranges
Connection IDsurvives an IP or port change, so Wi-Fi to cellular does not break
Congestion controlpluggable, CUBIC or BBR in userspace
Head-of-line blockingeliminated at the transport layer
Ossification resistanceheaders 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
SymptomLikely cause
%{http_version} is 1.1 on a modern serverALPN not offering h2, or a proxy downgrading
HTTP/2 works, HTTP/3 never negotiatesno Alt-Svc, or UDP 443 blocked on the path
Works on 1.1, breaks on 2a forbidden connection header, or mixed-case header names
Everything stalls together on a lossy linkTCP head-of-line blocking, the case for HTTP/3
421 Misdirected Requestconnection coalescing sent the request to the wrong origin