How HTTP carries every request and response
Four pieces in a question, four pieces in the answer. Thirty years of trying to make that exchange faster. The protocol of the web.
What is HTTP?
A request and a response, each built from the same four parts.
HTTP (Hypertext Transfer Protocol) is the application-layer protocol of the web. Request, response, headers, body, status code. Tim Berners-Lee drafted HTTP/0.9 in 1991; the current standards are HTTP/1.1 (RFC 9112, 2022), HTTP/2 (RFC 9113, 2022), and HTTP/3 (RFC 9114, 2022). HTTP is stateless by design. Cookies and tokens make it feel stateful.
HTTP is the conversation shape of the web. The client asks; the server answers. Both halves are built from the exact same four pieces, flipped.
Verb on a path.
A request line (GET /users/42 HTTP/1.1), then a block of headers, then a blank line, then an optional body. That's it. In HTTP/1.x it is all plaintext ASCII. You can read a live connection by eye.
Status on that verb.
A status line (HTTP/1.1 200 OK), then a block of headers, then a blank line, then the body. Same shape, flipped direction, different first line.
The server remembers nothing between requests. Every exchange is complete on its own. This is what makes HTTP so easy to cache, so easy to load-balance, and so easy to scale, and it's also why cookies, tokens, and sessions exist. See Part 08.
HTTP methods and status codes
The method says what you want; the status code says what happened.
The method declares intent. The status code reports outcome. The pair of them is how cache invalidators, API gateways, client libraries, and monitoring systems reason about what actually happened. Often without ever reading the body.
GET, HEAD.
Safe = has no side effect on the server. A GET must never create or mutate. Caches, prefetchers, and crawlers rely on this contract. HEAD is a GET without the body. Used to check existence or headers cheaply.
PUT, DELETE.
Repeat the same request N times; end state matches the single-request end state. This is what lets retries be safe: a failed PUT can be retried without creating a second resource. (POST is not idempotent — hence the double-submit bug.)
POST, PATCH.
POST creates subordinates. PATCH partially updates. Neither is safe; POST isn't idempotent. If you want at-most-once semantics, add an idempotency key header — Stripe-style.
The HTTP headers worth knowing
A small set does almost all the real work.
HTTP has hundreds of standardized headers. In practice a small set does almost all the work. Know these, and you can read most production traces.
- Host
Which site on this IP?
One IP can serve a thousand sites. Host is what tells the server which of them you meant. Without Host, virtual hosting doesn't work — which is why it's the one header HTTP/1.1 made mandatory.
- Content-Type
What am I looking at?
MIME type of the body. application/json, text/html; charset=utf-8, image/webp, multipart/form-data. Parsers key off this. Wrong Content-Type is the bug behind half the "why isn't my form submitting" tickets.
- Cookie
How statelessness becomes session.
The server Set-Cookies a value; the browser echoes it back on every subsequent request as Cookie: …. Combined with HttpOnly, Secure, and SameSite flags, this is how login works on top of a protocol with no memory.
- Accept-*
Content negotiation.
Accept: text/html, application/json;q=0.8 tells the server what the client can render. Accept-Encoding: gzip, br is why responses are compressed. Accept-Language drives i18n fallbacks. Quality values (q=) express preference, not hard requirement.
- Cache-Control
How long, and by whom.
public, max-age=3600 says "anyone can cache this for an hour." private restricts to the end user's browser only. no-store forbids caching entirely. Paired with ETag, this is the whole of HTTP caching. See the caching guide.
- Authorization
Watch a single HTTP request play out
The same exchange across HTTP/1.1, HTTP/2, and HTTP/3.
Before HTTP speaks a word, TCP opens the channel. Three packets, one round trip. On https, TLS adds another short handshake on top.
HTTP/1.1 and the keep-alive connection
Reusing one TCP connection was 1.1's biggest speedup over 1.0.
HTTP/1.0 opened a fresh TCP connection for every single request. One image tag was one handshake; a page with fifty assets was fifty handshakes. On 1990s modems, this was ruinous.
HTTP/1.1 shipped in 1997 with three changes that mattered: the Host header (so one IP could serve many sites), persistent connections via Connection: keep-alive (reuse the TCP connection for the next request), and pipelining (send the next request before the previous response arrives). Keep-alive was the big win. Pipelining, in practice, was broken almost everywhere because of head-of-line blocking. If the first response was slow, every subsequent response waited in line behind it.
Browsers worked around the pipelining problem by opening six parallel TCP connections per origin and round-robining requests across them. Simple, wasteful, and the actual state of the art until HTTP/2 arrived fifteen years later.
HTTP/2: many requests over one connection
Binary framing and streams let requests interleave instead of queueing.
Standardized as RFC 7540 in 2015, HTTP/2 rewrote the wire format from text to binary frames. The semantics — methods, status codes, headers. Stayed identical. The framing changed completely.
Many streams, one connection.
Every request is a stream with its own ID; responses interleave frames over the same TCP connection. No more six-parallel-connection hack, no more head-of-line blocking at the HTTP layer. A hundred requests fly at once over one socket.
Compressing repeated headers.
Most headers are identical across requests to the same origin — same User-Agent, same Cookie, same Accept. HPACK indexes them into a shared table; the wire only has to send deltas. Header overhead drops by an order of magnitude on real traffic.
HTTP/2 also shipped server push (proactively send assets the server knew the client would need). Which turned out to over-push, waste bandwidth, and mis-interact with caches. Chrome removed it in 2022. The feature is deprecated; the lesson that predicting client behavior is hard is permanent.
One problem HTTP/2 could not solve: it still runs over TCP, and TCP still has head-of-line blocking at the transport layer. One lost packet stalls every stream. Which is why HTTP/3 exists.
HTTP/3 moves to QUIC and UDP
Independent streams over UDP end TCP's transport-level head-of-line blocking.
HTTP/3 runs over QUIC, Google's transport protocol built on top of UDP (RFC 9000, 2021). The switch from TCP was deliberate: you cannot fix TCP's head-of-line blocking inside TCP itself, because kernels are contractually bound to deliver bytes in order. QUIC gives up that guarantee at the transport layer and re-adds it per-stream at the application layer.
Each request is an independent stream
QUIC streams have their own flow control and their own retransmission state. If a packet is lost, only the stream whose bytes were in that packet stalls; other streams keep flowing. HTTP-level multiplexing finally matches transport-level multiplexing.
Connection and encryption set up together
TLS 1.3 is welded into QUIC. There is no separate TCP handshake, no separate TLS handshake, no unencrypted QUIC. First connection: 1-RTT. Reconnection to a recent peer: 0-RTT. The request can ride alongside the ClientHello.
Survives switching Wi-Fi to cellular
TCP identifies a connection by source IP + port + destination IP + port. Change any one and the connection is dead. QUIC identifies connections by an opaque connection ID. Switch Wi-Fi to cellular in an elevator. The download never pauses.
HTTP is stateless, so cookies carry the session
The server forgets every request, so the browser keeps re-sending who you are.
HTTP has no memory. Every request arrives as if the server had never seen this client before. That property is what lets a request be routed to any server in a pool without coordination, and it's also what, naively, would make login impossible.
The fix is cookies. The server, after authenticating you, sends back Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax. The browser stores that, and from then on attaches it to every request to the same origin as Cookie: session=abc123. The server looks up abc123 in a session store (or decodes it as a signed JWT) and recovers who you are. Statelessness preserved at the protocol layer; state re-applied at the application layer.
The three flags matter. HttpOnly hides the cookie from JavaScript (no XSS exfiltration). Secure requires HTTPS (no downgrade leak). SameSite controls cross-site attachment (the modern defense against CSRF). Cookies without all three are an incident waiting to happen.
What an HTTP request looks like on the wire
Request line, headers, a blank line, then the body.
HTTP/1.1 is line-oriented ASCII. You can type one with a keyboard. The grammar fits in a paragraph: a request line, a stack of headers, a blank line, an optional body. Everything is human-readable. Which is why telnet on port 80 used to be the tutorial.
GET /api/users/42?include=email HTTP/1.1\r\n
Host: api.example.com\r\n
User-Agent: curl/8.6.0\r\n
Accept: application/json\r\n
Authorization: Bearer eyJhbGciOiJI…\r\n
Accept-Encoding: gzip, br\r\n
\r\n
# RESPONSE
HTTP/1.1 200 OK\r\n
Content-Type: application/json\r\n
Content-Length: 142\r\n
Cache-Control: private, max-age=60\r\n
ETag: "v3-c4ca4238"\r\n
Vary: Authorization, Accept-Encoding\r\n
\r\n
{"id":42,"name":"Ada Lovelace","email":"ada@example.com","created":"1815-12-10"} Three rules govern the framing. Lines end with CRLF (\\r\\n). Not just \\n; many parsers accept both, but the spec is strict. Headers are case-insensitive in name, opaque in value. A server that distinguishes Content-Type from content-type is broken. And the blank line is the boundary. Once a parser sees an empty CRLF, the body begins, and its length comes from Content-Length or chunked transfer encoding.
A single number, end-to-end.
The simplest framing: read exactly N bytes after the headers. Works only when the server knows the size in advance. If a header says Content-Length: 142 and the body is 141, the client hangs forever waiting for the missing byte. Content-Length smuggling is what happens when a frontend trusts one length and a backend trusts another — modern proxies reject ambiguity.
Streaming, in self-describing pieces.
Each chunk starts with its size in hex, then CRLF, then that many bytes, then CRLF. A zero-size chunk ends the stream. Used when the body is generated on the fly. Search results, video transcoding, server-sent events. The trade: cannot resume from a byte offset because no total length exists.
HTTP/2 is the same request line and headers, but binary-framed, length-prefixed, and split across many streams on one TCP connection. HPACK compresses headers against a shared dynamic table so repeats cost almost nothing. HTTP/3 carries those frames over QUIC instead of TCP and uses QPACK (HPACK rebuilt for out-of-order streams). The semantics. Methods, status codes, the meaning of Content-Type — are unchanged. Only the carriage.
How HTTP reuses one connection for many requests
Keeping the connection open avoids a fresh handshake every time.
A modern web page loads dozens of resources from the same origin. Tearing down a TCP+TLS connection between each one would cost a round-trip per asset. HTTP solves this in three different ways depending on the version, and gets it wrong twice before getting it right.
- 01
HTTP/1.0: one connection per request
The original. Open a TCP socket, send the request, receive the response, close the socket. Simple, terrible: a 100-asset page would open 100 connections, with 100 SYN/SYN-ACK round trips. Slow-start would never warm up. Browsers worked around it by opening 6 parallel connections per origin.
- 02
HTTP/1.1 keep-alive. Reuse, but serially
Connection: keep-alive kept the socket open for the next request. Better, but each request still had to finish before the next could start: a slow server response blocked everything behind it. Pipelining (sending requests without waiting) was specced but unusable. Most servers returned responses out of order and clients couldn't tell them apart. Head-of-line blocking at the application layer.
- 03
HTTP/2 streams — many requests interleaved
HTTP/2 introduced streams: each request/response pair gets a stream ID, and frames from any stream can be interleaved on the connection. A 50 KB image can be paused mid-flight while a 1 KB CSS file is delivered ahead of it. Priority hints (weight, dependency) let clients say which assets matter most. One TCP socket; everything in parallel.
- 04
HTTP/2's hidden flaw — TCP head-of-line returns
TCP delivers bytes in order. If one packet from stream #3 is lost, every byte after it on the same connection is delayed. Even bytes belonging to streams #5 or #7. HTTP/2 multiplexed at the application layer; TCP serialized at the transport layer. On a flaky cellular link, HTTP/2 can be slower than HTTP/1.1's six parallel sockets.
- 05
HTTP/3 + QUIC — multiplexing all the way down
QUIC is HTTP/2's frame model rebuilt directly on UDP, with each stream having its own ordering and loss recovery. A dropped packet on stream #3 only stalls stream #3. Connection migration also moves with the user. Switch from Wi-Fi to LTE without reopening the connection, because the connection ID is independent of the IP. The HOL problem is finally a per-stream problem.
| Property | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Wire format | ASCII text | Binary frames | Binary frames over QUIC |
| Transport | TCP | TCP | UDP (QUIC) |
| Multiplexing | No (serial keep-alive) | Yes (streams) | Yes (streams + per-stream loss) |
| Header compression | None | HPACK | QPACK |
| TLS | Optional | Required (in browsers) | Required (1.3 only, baked in) |
| Connection migration | No | No | Yes (connection ID) |
Step through the full request lifecycle
Follow one request from connect to response, stage by stage.
When something is wrong with HTTP, the answer is almost always in the headers. Five tools surface them at different layers. Pick whichever matches the question.
Scriptable requests from the terminal
curl -v -o /dev/null https://example.com/api shows the request line, every header sent and received, the status code, and the timing. --http2 / --http3 force the version. --write-out '%{time_total}s\n' emits the wall-clock time. -H "X-Forwarded-For: 1.2.3.4" overrides any header you want to test.
curl, but easier to read
http GET api.example.com/users id==42 Authorization:"Bearer …" formats JSON, colours headers, follows redirects. Pairs well with curl: use httpie when you want to read a response, curl when you want to script it.
Every request the browser made
Chrome and Firefox DevTools' Network panel groups every request the page made. The Timing tab breaks down DNS / TLS / wait / download for each. Initiator shows which line of JS triggered it. Throttle reproduces a slow 3G link. Right-click → Copy as curl turns any request into a reproducible terminal command.
1xx. Informational. 100 Continue means "I read your headers, send the body." 101 is the WebSocket upgrade. You almost never need these.
2xx — fine. 200 for "here's the resource"; 201 Created after a POST that made something; 204 No Content when there's nothing to return; 206 Partial for byte-range requests.
3xx. Go elsewhere. 301 permanent vs 302/307 temporary; 304 Not Modified is the cache-revalidation success path; 308 is the modern permanent redirect that doesn't change the method.
4xx — your fault. 400 malformed; 401 not authenticated; 403 authenticated but not allowed; 404 nothing here; 409 conflict; 422 well-formed but semantically wrong; 429 rate-limited.
5xx. Server's fault. 500 generic crash; 502 the upstream is broken; 503 overloaded; 504 upstream timed out. The difference between 502 and 504 is the difference between "got an error from the next hop" and "the next hop never replied". Both common in load-balancer logs.
The production header checklist
The headers that matter beyond Content-Type.
- Cache-Control
- public, max-age=31536000, immutable for hashed-name static assets; no-store for sensitive responses; stale-while-revalidate for graceful refresh.
- Strict-Transport-Security
- max-age=63072000; includeSubDomains; preload. Tells browsers to always use HTTPS. Submit your domain to the HSTS preload list once stable.
- Content-Security-Policy
- The single most effective XSS mitigation. Strict CSP (script-src self with nonces) plus object-src none blocks ~95% of injection attacks.
- Cross-Origin-Opener-Policy + Cross-Origin-Embedder-Policy
- Required for SharedArrayBuffer and high-precision timers. Production: same-origin + require-corp.
- Referrer-Policy
- strict-origin-when-cross-origin (the modern browser default) usually right; no-referrer for sensitive contexts.
- X-Content-Type-Options: nosniff
- Forces browsers to honour your declared Content-Type. Always on.
- Permissions-Policy
- Replaces the older Feature-Policy. Disables APIs your app doesn't need (camera, microphone, geolocation) so an injected script can't enable them.
Three free tools to verify: securityheaders.com grades you on security headers. hstspreload.org checks HSTS readiness. csp-evaluator.withgoogle.com reviews your CSP for common bypasses.
HTTP started as eighteen lines of pseudocode on Tim Berners-Lee's NeXT machine and now carries almost every byte of human communication. The shape of it. A request, a response, four pieces each — hasn't changed in thirty-five years. Everything that has changed is underneath: text → binary, one connection → many streams, TCP → QUIC. Each rewrite chased the same adversary, latency, from a different angle. The protocol is boring. The boringness is the feature.
Three quick checks before you close the tab.
Pick an answer for each. The right one reveals a short explanation; the wrong ones do too. There is no scoring. These are here so you can confirm the mental model travels with you when you next open DevTools.
Read
further.
- MDNHTTP. The MDN ReferenceThe most careful, browser-accurate reference on the web. Every header, every status code, every caveat.
- IETF · RFC 9110HTTP SemanticsThe 2022 re-specification. Version-independent semantics — methods, status codes, headers — in one place.
- IETF · RFC 9113HTTP/2Binary framing, streams, HPACK. Replaces RFC 7540 with the corrections accumulated since.
- IETF · RFC 9114HTTP/3How HTTP maps onto QUIC. Companion to RFC 9000 (the QUIC transport itself).
- IETF · RFC 9000QUIC — A UDP-Based Multiplexed and Secure TransportThe transport underneath HTTP/3. Long, but the rationale sections are unusually honest.
- web.devHTTP/2 and youPractical engineering notes. What changed for front-end performance, what stopped being necessary.
- IETF · RFC 7541HPACK. Header Compression for HTTP/2The static + dynamic Huffman table behind Part 10's "header compression". Short and surprisingly readable.
- Cloudflare blogThe Road to QUICA field report on what was actually hard about deploying HTTP/3. Middleboxes, kernel APIs, and CPU usage.
- http.devA working reference site for HTTPSearchable index of every status code, header, and method. With the precise spec citation for each.
- IETF · RFC 6265HTTP State Management MechanismThe cookies spec — origin scoping, SameSite, Secure, HttpOnly. The pragmatic lifeline for the stateless protocol of Part 08.
- Daniel StenbergEverything curlA book-length manual by the author of curl. The single best place to learn the Part 11 toolbox in depth.
- Cloudflare blogHead-of-line blocking in QUIC and HTTP/3, the detailsAn end-of-the-protocol-stack explainer for the same Part 10 phenomenon. Including the surprising places it still bites.