Concept
HTTP is the application-layer protocol that makes the web work. Its evolution from 1.1 → 2 → 3 is a story of progressively eliminating bottlenecks, and understanding each version tells you why certain frontend performance techniques work.
HTTP/1.1 (1997, still dominant for many services)
HTTP/1.1 introduced persistent connections (Connection: keep-alive). Before that, each request opened and closed a TCP connection. With keep-alive, multiple requests could reuse one connection, but only one request at a time per connection (sequential).
The practical problem: browsers open 6 connections per origin to parallelise downloads. This caused:
- Domain sharding (hosting assets on 4+ subdomains) to get 24+ parallel connections, a hack that HTTP/2 made unnecessary and actually harmful
- Request bundling (spritesheet images, CSS/JS concatenation) to reduce request count, also now unnecessary
- Head-of-line blocking at the HTTP level: request B waits for request A's full response before it starts
HTTP/1.1 request/response format
GET /api/users HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Accept: application/json
Connection: keep-alive
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 234
Cache-Control: max-age=60
{"users": [...]}Headers are plain text, repeated on every request/response. A typical request has 500, 800 bytes of headers. For small API responses, headers can outweigh the body.
HTTP/2 (2015, now dominant)
HTTP/2 keeps the same semantics as HTTP/1.1 (same methods, status codes, headers) but dramatically changes the transport:
Multiplexing
Multiple requests/responses over one TCP connection, interleaved as frames. Request B doesn't wait for response A to complete. This eliminates HTTP-level head-of-line blocking and makes domain sharding and large bundle concatenation counterproductive.
HTTP/1.1: HTTP/2:
Connection 1: [A → a] Connection 1: [A → B → C → a → b → c]
Connection 2: [B → b] (interleaved frames)
Connection 3: [C → c]Header compression (HPACK)
HPACK maintains a shared header table between client and server. Repeated headers (e.g., Accept, User-Agent) are sent as indices into the table rather than full strings. Headers shrink from ~500 bytes to ~50 bytes for subsequent requests.
Binary framing
HTTP/2 is binary, not text. Data is split into frames (HEADERS frame, DATA frame) which can be interleaved. This enables multiplexing but makes manual debugging harder (you need Wireshark or a proxy).
HTTP/2 streams and priority
Each request is a stream with an ID. Streams can be assigned weights and dependencies for prioritisation, the server can send the CSS response before the image response even if the image was requested first. Browser implementations of prioritisation vary.
The remaining bottleneck: TCP head-of-line blocking
HTTP/2 multiplexes over one TCP connection. TCP guarantees ordered delivery. If one TCP packet is lost, all streams on that connection stall until the packet is retransmitted. On a lossy network (mobile, WiFi), HTTP/2 can actually be slower than HTTP/1.1's multiple connections.
HTTP/3 (2022, growing fast)
HTTP/3 replaces TCP with QUIC (originally by Google, now an IETF standard).
QUIC fundamentals
QUIC runs over UDP (unreliable) and implements reliability, ordering, and congestion control at the application layer, but per-stream, not per-connection. A lost packet only stalls the stream it belongs to, not other streams. This eliminates TCP's head-of-line blocking.
TCP (HTTP/2): All streams stall if one packet is lost
QUIC (HTTP/3): Only the affected stream stalls0-RTT connection establishment
QUIC combines the transport handshake and TLS handshake into one round trip for new connections (vs TCP's 2 RTTs: TCP handshake + TLS). For returning clients with a session ticket, QUIC achieves 0-RTT, data can be sent with the first packet.
Connection migration
QUIC connections are identified by a Connection ID, not by IP+port. When a mobile user switches from WiFi to LTE, the connection seamlessly migrates without re-handshaking. HTTP/2 over TCP would drop the connection.
HTTP Methods
| Method | Idempotent | Safe | Body | Use |
|---|---|---|---|---|
| GET | ✓ | ✓ | No | Read resource |
| POST | ✗ | ✗ | Yes | Create/submit |
| PUT | ✓ | ✗ | Yes | Full replace |
| PATCH |
Idempotent means calling it N times has the same effect as calling it once. A POST that creates a record is not idempotent, retrying creates duplicates (use idempotency keys for safe retries).
Status Codes (the important ones)
1xx Informational
100 Continue (request body is large; server says "send it")
101 Switching Protocols (WebSocket upgrade)
2xx Success
200 OK
201 Created (POST)
204 No Content (DELETE or PUT with no body)
206 Partial Content (range requests, streaming)
3xx Redirection
301 Moved Permanently (cached by browser, hard to undo!)
302 Found (temporary redirect)
304 Not Modified (conditional GET, cache is valid)
307 Temporary Redirect (method preserved)
308 Permanent Redirect (method preserved)
4xx Client errors
400 Bad Request
401 Unauthorized (not authenticated)
403 Forbidden (authenticated but not authorized)
404 Not Found
405 Method Not Allowed
409 Conflict
410 Gone (permanent 404, for SEO)
422 Unprocessable Entity (validation errors)
429 Too Many Requests (rate limiting)
5xx Server errors
500 Internal Server Error
502 Bad Gateway
503 Service Unavailable
504 Gateway TimeoutCaching headers
Cache-Control: max-age=3600 Fresh for 1hr; reuse without network
Cache-Control: no-cache Revalidate every time (may return 304)
Cache-Control: no-store Never cache
Cache-Control: immutable Never revalidate (use with hash in URL)
Cache-Control: stale-while-revalidate=60 Serve stale, refresh in background
ETag: "abc123" Server fingerprint; client sends If-None-Match
Last-Modified: <date> Client sends If-Modified-Since
Vary: Accept-Encoding Cache separately per encodingCommon Mistakes
1. Domain sharding in an HTTP/2 world
HTTP/1.1: domain sharding (4 CDN subdomains = 24 parallel connections) was a legitimate optimization. HTTP/2: it creates extra DNS lookups, TCP connections, and TLS handshakes, killing performance. Verify your server speaks HTTP/2 before applying HTTP/1.1 tricks.
2. Using 301 instead of 302 for temporary redirects
301 is cached by browsers indefinitely with no expiry. If you redirect example.com/old → example.com/new with a 301 and later want to undo it, users who cached the redirect will never see the undo. Use 302 or 307 for temporary redirects; use 301 only when you're certain.
3. Confusing 401 and 403
- 401 Unauthorized: The user is not authenticated. The response should include a
WWW-Authenticateheader. - 403 Forbidden: The user is authenticated but lacks permission.
Using 401 when the user is logged in confuses clients (they'll prompt for login again).
4. Ignoring Vary headers for cached API responses
If an API returns different content based on Accept-Language but doesn't include Vary: Accept-Language, a CDN will serve the first cached response to all users regardless of language.
5. Not setting Cache-Control at all
Browsers use heuristic caching when Cache-Control is absent (typically 10% of Last-Modified age). This leads to unpredictable caching. Always set Cache-Control explicitly.
Best Practices
- Upgrade to HTTP/2 or HTTP/3. Verify with
curl -I --http2 https://yoursite.comor check the "Protocol" column in DevTools Network tab. - Set
Cache-Control: immutableon content-hashed static assets (e.g.,app.a3b2c1.js). Browsers will never revalidate them. - Use
ETag+If-None-Matchfor API responses that change infrequently, allows 304 responses instead of full payloads. - Return
429withRetry-Afterheader when rate limiting. Clients can back off intelligently. - Use for DELETE and PATCH responses with no body, don't return with an empty .
Performance Tips
- HTTP/2 multiplexing makes many small requests almost as cheap as one large request. This means code splitting is safe.
- HTTP/3's 0-RTT is especially valuable for mobile users and APIs with high request frequency (each API call saves 1 RTT).
Early Hints (103): Server sends link preload headers before it finishes building the HTML response. Chrome supports it; Cloudflare and Vercel support it server-side.- Brotli compression (
Content-Encoding: br) achieves 15, 25% better compression than gzip. All modern browsers support it. Set it up on your CDN/server.
