11 min read · Guide · Network
How it works · Network · Protocols

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.

Parts01 – 08 InteractiveHTTP/1.1 · 2 · 3 PrereqTCP / DNS

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.

Request

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.

Response

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.

Stateless, by design

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.

SAFE

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.

IDEMPOTENT

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.)

MUTATING

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.

1xxInformational. 100 Continue, 101 Switching Protocols (WebSocket upgrade). Rare in the wild.
2xxSuccess. 200 OK, 201 Created, 204 No Content (success, empty body), 206 Partial Content (range requests. Video seek).
3xxRedirection. 301 permanent, 302 temporary, 304 Not Modified (conditional GET match. Cache hit).
4xxClient fault. 400 malformed, 401 unauthenticated, 403 forbidden, 404 not found, 409 conflict, 429 too many requests.
5xxServer fault. 500 generic, 502 bad gateway (upstream broken), 503 unavailable (overloaded), 504 gateway timeout.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. Authorization

    Who is asking.

    Authorization: Bearer eyJhbGciOi… carries an OAuth token or API key. Basic auth still exists but is effectively plaintext (base64 is not encryption). Always require HTTPS when this header appears.


Watch a single HTTP request play out

The same exchange across HTTP/1.1, HTTP/2, and HTTP/3.

Toggle between HTTP/1.1, HTTP/2, and HTTP/3 below and watch the same logical exchange play out at each layer. The intent is identical. Get the homepage. Everything underneath has been rewritten three times, each time to fix a different kind of slowness.

CLIENTBrowser198.51.100.24SERVERexample.com93.184.216.34 01TCP connect 02 Request line + headers 03 Status line + headers 04 Response body 05 Connection: keep-alive HTTP/1.1 · TEXT OVER TCP
Step 01 of 05

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.

Multiplexing

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.

HPACK

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.

Independent

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.

1-RTT / 0-RTT

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.

Migration

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.

Content-Length

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.

Transfer-Encoding: chunked

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 keeps the model, drops the text

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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 formatASCII textBinary framesBinary frames over QUIC
TransportTCPTCPUDP (QUIC)
MultiplexingNo (serial keep-alive)Yes (streams)Yes (streams + per-stream loss)
Header compressionNoneHPACKQPACK
TLSOptionalRequired (in browsers)Required (1.3 only, baked in)
Connection migrationNoNoYes (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.

curl

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.

httpie

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.

DevTools. Network

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.

Status code field guide

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.



A closing note

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.

Q1. A browser sends a conditional GET and receives 304 Not Modified. What just happened?
Q2. How does HTTP/2 multiplexing differ from HTTP/1.1 pipelining?
Q3. A site wants browsers to refuse plaintext HTTP for the next year, even if the user types `http://`. Which header solves this?

Read
further.

Found this useful?