Networking · HTTP

HTTP: what a request really carries

The request and response as text, methods and status codes, the headers that matter, how connections evolved from one request at a time to HTTP/3, caching, cookies, and HTTPS.

Updated 2026-09-08
On this page

HTTP is a few lines of text. A client writes "give me this thing", a server writes "here it is" or "no", and both add some labelled lines of context. Everything else on the web, from a page load to an API call to a video stream, is built by putting more into those lines. This page reads the lines, then follows how the connection underneath them changed while the lines stayed the same.

The map

Read this first when short on time. Every branch is a section below.

Figure 1. The whole article on one page. Every branch is a section below; fold what you know, open what you do not.

A request and a response, in plain text

HTTP/1.1 is text you can type. A request is a line saying what you want, then any number of header lines, then a blank line, then an optional body. A response is a line saying how it went, then headers, blank line, body. Here is the whole exchange for a page:

GET /index.html HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: text/html

HTTP/1.1 200 OK
Date: Tue, 08 Sep 2026 10:15:02 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1256
Cache-Control: max-age=600

<!doctype html>
<html>...
  1. The request line: a method (GET), a path (/index.html), a version. The path is everything after the host in the URL, including the query string.
  2. Headers: one Name: value per line. Names are case-insensitive. Host is the only one HTTP/1.1 requires, because one server address may host many sites and this is how it picks.
  3. A blank line ends the headers. If there is a body, Content-Length says how many bytes follow, or Transfer-Encoding: chunked says it comes in pieces, each with its own length, ending with a zero-length piece. Without one of those the receiver cannot know where the message ends, because TCP has no boundaries, as the TCP article explains.
  4. The status line of the response: version, a three-digit code, and a phrase nobody's software reads.

HTTP is stateless. The server handles each request on its own and keeps no memory of the previous one from the same client. Two requests from you are, to the protocol, two requests from anyone. Everything that feels like a session, staying logged in, a shopping cart, is done by the client sending a token with every request, described in the cookies section.

sequenceDiagram
  autonumber
  participant B as Browser
  participant S as Server
  B->>S: GET /index.html, Host: example.com
  S-->>B: 200 OK, text/html, 1256 bytes
  B->>S: GET /style.css
  S-->>B: 200 OK, text/css
  B->>S: GET /logo.png
  S-->>B: 200 OK, image/png
  B->>S: POST /api/login, body: credentials
  S-->>B: 200 OK, Set-Cookie: session=abc123
Figure 2. A page load is many exchanges. Each one is independent; the server does not know that requests 3 and 5 came from the same page until a cookie ties them together at step 8.
In the wild
  • curl -v https://example.com prints the request it sends with a > prefix and the response headers with <. It is the fastest way to see exactly what a server does.
  • Browser devtools, network tab, show every request with its headers, timing and size. Right-click gives "Copy as cURL" to reproduce it in a terminal.
  • HTTP/2 and HTTP/3 are not text on the wire, but every tool shows them as if they were, because the meaning is identical.
Header
A Name: value line carrying context about the request or response.
Body
The payload after the headers: HTML, JSON, an image, form data.
Stateless
Each request is handled without memory of earlier ones. State is carried by the client.

Methods and status codes

The method says what kind of thing the request is. The status code says what kind of thing happened. Both are small vocabularies worth knowing exactly, because clients, proxies and caches make decisions from them without reading the body.

MethodMeansSafeIdempotentBody
GETGive me this. The one caches understand.YesYesNo
HEADLike GET but only the headers. Checking size or freshness.YesYesNo
POSTHere is something: create it, process it, act on it.NoNoYes
PUTReplace what is at this path with this body.NoYesYes
PATCHChange part of what is at this path.NoNoYes
DELETERemove what is at this path.NoYesRarely
OPTIONSWhat may I do here? Used by browsers before cross-site requests.YesYesNo

The two promises matter more than the names. Safe means the request changes nothing on the server, so a browser may prefetch it and a cache may store it. Idempotent means doing it twice has the same effect as doing it once: a second PUT of the same body, or a second DELETE, changes nothing more. A client that lost the connection mid-request may retry an idempotent one blindly. It must not retry a POST, because the first one may have gone through, which is why payment APIs ask for an idempotency key, as the reliability article covers.

CodeMeansNotes
200 OKHere is what you asked forThe body is the answer
201 CreatedYour POST made somethingUsually with a Location header pointing at it
204 No ContentDone, nothing to returnCommon for DELETE
301 / 308Moved permanentlyBrowsers and search engines remember it. 308 keeps the method
302 / 307Moved for nowCome back to the original next time. 307 keeps the method
304 Not ModifiedYour cached copy is still goodNo body. The point of ETags
400 Bad RequestThe request itself is malformedDo not retry without changing it
401 UnauthorizedWho are you? Credentials missing or wrongReally means "unauthenticated"
403 ForbiddenI know who you are and noCredentials will not help
404 Not FoundNothing at this pathAlso used to hide things that exist
429 Too Many RequestsSlow downWith Retry-After. Rate limiting
500 Internal Server ErrorThe server brokeA bug or crash on their side
502 Bad GatewayThe proxy could not get a valid answer from the backendThe backend is down or misbehaving
503 Service UnavailableOverloaded or in maintenanceRetry later. Often with Retry-After
504 Gateway TimeoutThe backend took too longThe proxy gave up waiting

The first digit is what most software acts on: 2 is success, 3 is go somewhere else, 4 is the client's mistake and retrying the same thing will not help, 5 is the server's problem and a retry later might. A load balancer counting 5xx responses to decide a backend is unhealthy is reading only that digit.

In the wild
  • REST APIs are the method table applied to resources: GET /users/42, PUT /users/42, DELETE /users/42, POST /users to create.
  • Retry libraries and cloud SDKs retry 429, 502, 503 and 504 with backoff, and refuse to retry a POST unless told it is idempotent.
  • A 502 from a CDN almost always means the origin is down; a 503 from it often means the CDN is protecting the origin from a traffic spike.
Safe method
One that changes nothing on the server. GET and HEAD. Caches and prefetchers rely on it.
Idempotent method
One that has the same effect done once or many times. GET, PUT, DELETE. Safe to retry.

The headers that matter

There are hundreds of headers. About fifteen of them do almost all the work, and they fall into a few jobs.

JobHeaderWhat it does
RoutingHostWhich site, when one address serves many. Required in every request.
X-Forwarded-For, ForwardedAdded by proxies so the backend can see the real client address.
The bodyContent-TypeWhat the body is: text/html, application/json, image/png, multipart/form-data.
Content-LengthHow many bytes, so the receiver knows where the message ends.
Transfer-Encoding: chunkedBody sent in length-prefixed pieces, for when the size is not known up front.
NegotiationAcceptWhat the client can handle: application/json, image/webp.
Accept-Encoding / Content-EncodingClient can take gzip, br; server says the body is compressed with one of them.
Accept-LanguagePreferred languages, for sites that translate.
IdentityAuthorizationCredentials: Bearer token, or Basic base64(user:pass).
Cookie / Set-CookieThe client returns what the server gave it earlier.
User-AgentWhat software is asking. Widely lied about.
CachingCache-ControlHow long and by whom the response may be cached.
ETag / If-None-MatchA version tag and a conditional request that returns 304 if unchanged.
VaryWhich request headers change the response, so caches keep separate copies.
ControlLocationWhere to go, on a redirect or after a 201.
Retry-AfterSeconds to wait, on 429 and 503.

Two details trip people up. Compression happens per response: the server compresses the body, says so with Content-Encoding, and Content-Length is the compressed size. And Content-Type is the server's claim about the body, which browsers mostly trust; a JSON API that forgets to send application/json gets its output rendered as text.

In the wild
  • nginx and Cloudflare add X-Forwarded-For on the way to the origin; without it every request looks like it came from the proxy, and rate limits and logs break.
  • Brotli (br) compresses text 15 to 20 percent smaller than gzip and is what large sites serve to any browser that advertises it.
  • CORS is a set of headers (Origin, Access-Control-Allow-Origin) with which a server tells browsers which other sites may call it from JavaScript; the OPTIONS preflight is the browser asking first.
Content negotiation
The client saying what it accepts and the server choosing a representation to match.
Chunked encoding
Sending a body as a series of length-prefixed pieces, so the total size need not be known in advance.

Connections: from one at a time to HTTP/3

The text of HTTP has barely changed since 1997. What changed, three times, is how requests share the connection underneath. Each change was about the same thing: a page has a hundred resources and a round trip is expensive, so how many requests can be in flight at once?

flowchart TD
  A["HTTP/1.0: one connection per request, handshake every time"] --> B["HTTP/1.1: keep-alive, one request at a time per connection, so browsers open 6"]
  B --> C["HTTP/2: one connection, requests interleaved as binary frames, headers compressed"]
  C --> D["HTTP/3: same idea on QUIC, so one lost packet stalls one request instead of all"]
Figure 3. Four generations, one goal: more requests in flight per round trip. The request and response lines are the same in all four.

HTTP/1.1 keeps the TCP connection open after a response so the next request skips the handshake. But on one connection only one request can be outstanding: send, wait for the full response, send the next. A slow response holds up everything queued behind it. Browsers work around this by opening about six connections per host and spreading requests across them, which is six handshakes and six congestion windows warming up.

HTTP/2 puts everything on one connection. Requests and responses are cut into binary frames, each tagged with a stream number, and frames from different streams are interleaved, so a hundred requests can be in flight at once and responses arrive in whatever order they are ready. Headers, which repeat almost identically on every request, are compressed with a shared dictionary (HPACK). The text you see in devtools is a rendering; the wire is binary. Its one weakness is the TCP stream under it: as the TCP article explains, one lost packet stops every stream on the connection until the retransmit arrives.

HTTP/3 is HTTP/2's design on QUIC. Each request is a QUIC stream, so a loss stalls only that request, the handshake is one round trip shorter, and the connection survives the phone changing networks. Browsers discover it through an Alt-Svc header on an earlier response and fall back to HTTP/2 when UDP is blocked.

HTTP/1.1HTTP/2HTTP/3
Wire formatTextBinary framesBinary frames
Requests in flight per connection1Many (typically 100)Many
Connections a browser opens per hostAbout 611
Header compressionNoneHPACKQPACK
One lost packet stallsThat connection's current requestAll streams on the connectionOne stream
TransportTCPTCPQUIC over UDP
EncryptionOptional (https)Required by every browserAlways
In the wild
  • Most websites serve HTTP/2, and the largest serve HTTP/3; devtools' protocol column shows h2 or h3.
  • Old performance tricks such as sprite sheets, concatenated JavaScript bundles and domain sharding existed to work around HTTP/1.1's one-at-a-time limit, and HTTP/2 made them unnecessary or harmful.
  • Between a CDN and an origin HTTP/1.1 over a pool of warm connections is still common, because the latency is low and the simplicity is worth it.
  • gRPC is HTTP/2 frames carrying protobuf, using streams for its long-lived bidirectional calls.
Watch out

HTTP/2 server push, where the server sent resources before the browser asked, was removed from Chrome in 2022 because it rarely helped and often sent things the browser already had. 103 Early Hints with Link: rel=preload replaced it: the server hints, the browser decides.

Keep-alive
Reusing one TCP connection for several requests instead of opening one per request.
Multiplexing
Interleaving many requests and responses on one connection as tagged frames.
HPACK / QPACK
Header compression using a dictionary shared between the two ends.

Caching: the request that never happens

The fastest request is the one the browser does not send. HTTP caching is a set of headers with which a server tells clients and proxies how long a response stays valid, and how to check cheaply when it might not.

flowchart TD
  A["Browser needs /app.js"] --> B{"Cached copy?"}
  B -- no --> F["GET /app.js, store the response with its headers"]
  B -- yes --> C{"Still fresh?"}
  C -- yes --> D["Use it. No request at all."]
  C -- no --> E["GET /app.js with If-None-Match: the ETag"]
  E --> G{"Server: changed?"}
  G -- no --> H["304 Not Modified, tiny. Reuse the copy, reset freshness"]
  G -- yes --> I["200 OK with the new body and a new ETag"]
Figure 4. Two layers. Freshness avoids the request entirely; validation makes the request cheap when freshness has run out.

Freshness is set by Cache-Control. max-age=600 means "good for ten minutes, do not even ask". no-cache means "you may store it but always validate before using it". no-store means "never write this to disk", for private data. private means only the user's browser may cache it, not a shared CDN; public allows both. immutable means "this URL's content will never change", which is true for files named by their hash, and lets the browser skip validation forever.

Validation uses a tag. The server sends an ETag, an opaque version string, usually a hash. When the copy goes stale the client sends If-None-Match with that tag. If the content is unchanged the server answers 304 Not Modified with no body, a few hundred bytes instead of the whole file, and the client keeps its copy. Last-Modified and If-Modified-Since are the older form, with timestamps.

Where caches live: the browser, a CDN, a reverse proxy in front of the application, and sometimes a corporate proxy. All obey the same headers. Vary: Accept-Encoding tells them the gzip and brotli versions are different copies. A shared cache serving one user's private response to another is the classic caching security bug.

In the wild
  • Hashed asset names like app.3f9a1c.js with max-age=31536000, immutable are the standard front-end pattern: the file never changes, so the browser never asks, and a new build gets a new name.
  • CDN purge is how a site invalidates a cached page before its max-age runs out; without a purge, a long max-age on HTML means users see stale pages.
  • stale-while-revalidate lets a cache serve the old copy immediately while fetching a fresh one in the background, which is how news sites feel fast and stay current.
  • API responses are usually Cache-Control: no-store, which is why a GET to an API is slower than a GET to a static file even when the JSON never changes; adding an ETag is the cheap fix.
Fresh
A cached response still within its max-age, usable without asking the server.
Validation
Asking the server whether a stale copy is still correct, using ETag or Last-Modified.
ETag
An opaque version identifier for a response, compared by the server on a conditional request.

State on a stateless protocol: cookies and tokens

Since the server remembers nothing between requests, the client has to carry the memory. A cookie is a value the server hands the client in a response, which the client sends back on every later request to the same site. Whatever the server keys on that value, a login, a cart, a preference, becomes the session.

HTTP/1.1 200 OK
Set-Cookie: session=8f3a2c9e; Path=/; Max-Age=86400; HttpOnly; Secure; SameSite=Lax

GET /account HTTP/1.1
Host: example.com
Cookie: session=8f3a2c9e

The attributes are the security. HttpOnly keeps JavaScript from reading the cookie, so a script injected into the page cannot steal it. Secure sends it only over HTTPS. SameSite controls whether it is sent when another site triggers the request; Lax allows top-level navigations, Strict allows none, and this is the main defence against cross-site request forgery. Max-Age or Expires makes it persist; without them it dies with the browser session. Domain and Path scope where it is sent.

APIs called from code rather than browsers usually skip cookies and send a bearer token in the Authorization header instead. The token may be an opaque string the server looks up, or a signed JWT the server verifies without a lookup. The difference between the two, and how sessions are stored at scale, is in the security article.

In the wild
  • Every login on the web is a Set-Cookie after a successful POST; logging out is a Set-Cookie with Max-Age=0.
  • Browsers default SameSite to Lax since 2020, which quietly fixed a class of CSRF attacks and broke some cross-site embedded widgets.
  • Cloud APIs take Authorization: Bearer with a short-lived token, refreshed by the client, so a leaked token is only useful for minutes.
Watch out

A cookie without Secure is sent over plain HTTP too, and a cookie without HttpOnly is readable by any script on the page, including a compromised third-party one. Session cookies should have both, always.

Cookie
A value set by the server and returned by the client on subsequent requests to the same site.
Bearer token
A credential sent in the Authorization header; whoever holds it is authenticated.
CSRF
Cross-site request forgery: another site making the browser send a request with the user's cookies. SameSite blocks it.

HTTPS: the same HTTP inside TLS

HTTPS is not a different protocol. It is HTTP written into a TLS connection instead of a raw TCP one, on port 443 instead of 80. The request and response are unchanged. What changes is that nobody between the two ends can read or alter them, and the client knows it is talking to the real owner of the name.

The TLS handshake, summarised in the packet journey, does two things before the first byte of HTTP. It agrees on keys, so the bytes are encrypted. And the server presents a certificate, signed by an authority the browser trusts, saying that this public key belongs to example.com. The browser checks that the name on the certificate matches the name it asked for. A mismatch, an expired certificate, or an unknown authority gives the warning page.

One subtlety: because Host is inside the encrypted request, a server hosting many sites on one address could not know which certificate to present. So the client sends the name in the clear at the start of the handshake, in the SNI extension, and that is the one part of an HTTPS request an observer can still see. Encrypted Client Hello is the fix, and it is rolling out slowly.

In the wild
  • Let's Encrypt issues free certificates valid for 90 days, renewed automatically by a small client, which is why nearly every site is HTTPS now.
  • HSTS is a response header (Strict-Transport-Security) telling the browser to never use plain HTTP for this site again, even if a link says http://.
  • TLS termination at a load balancer or CDN means the encrypted connection ends there and plain HTTP or a second TLS connection continues to the origin, as the edge article describes.
TLS
Transport Layer Security. Encryption and authentication under HTTP, making it HTTPS.
Certificate
A signed statement binding a public key to a domain name, issued by an authority the client trusts.
SNI
Server Name Indication: the hostname sent in the clear at the start of TLS so the server can pick the right certificate.

Recap

  • A request is a method, a path and a version, then headers, a blank line, and maybe a body. A response is a version, a status code, headers, blank line, body.
  • HTTP is stateless. Sessions are the client sending a cookie or token on every request.
  • GET and HEAD are safe; GET, PUT and DELETE are idempotent and may be retried; POST is neither.
  • Status codes by first digit: 2 success, 3 redirect, 4 your fault, 5 the server's. 304 is a validated cache hit, 401 means unauthenticated, 429 means slow down, 502 and 504 mean the backend behind a proxy failed.
  • Host routes, Content-Type and Content-Length describe the body, Accept-Encoding negotiates compression, Authorization and Cookie identify, Cache-Control and ETag cache.
  • HTTP/1.1 allows one request at a time per connection, so browsers open six. HTTP/2 multiplexes many on one connection with binary frames and compressed headers, but one TCP loss stalls all of them. HTTP/3 runs the same design on QUIC.
  • Caching has two layers: freshness (max-age) avoids the request, validation (ETag, 304) makes it cheap. Hashed filenames with immutable are the standard pattern.
  • Cookies need HttpOnly, Secure and SameSite. APIs use bearer tokens in Authorization.
  • HTTPS is HTTP inside TLS on port 443: encrypted, and authenticated by a certificate whose name must match. SNI sends the name in the clear.

Questions

Try answering each one out loud before opening it. Lead with the one-line answer, then a couple of points, then one extra detail.

What is in an HTTP request and an HTTP response?

A request is a line with the method, path and version, then header lines, a blank line, and an optional body; a response is a line with the version, status code and phrase, then headers, a blank line, and the body.

  • Host is the only required request header in HTTP/1.1.
  • The body's end is marked by Content-Length or chunked encoding, because TCP has no message boundaries.

HTTP/2 and HTTP/3 carry the same fields in binary frames, and tools render them back as text.

What does it mean that HTTP is stateless, and how do logins work anyway?

The server handles each request without memory of previous ones from the same client; a login works because the server sends a cookie or token after the credentials are accepted, and the client includes it on every later request, so the server can look the session up each time.

  • Statelessness is what lets any server in a pool handle any request.
  • The session data itself lives in a store the servers share, or inside a signed token.

Logging out is a Set-Cookie that expires the cookie, or the server invalidating the token.

Safe versus idempotent: what is the difference and why does it matter?

Safe means the request changes nothing on the server (GET, HEAD), so it may be cached and prefetched; idempotent means repeating it has no additional effect (GET, PUT, DELETE), so a client may retry it after a lost connection. POST is neither, so a blind retry can duplicate an action.

  • Retry logic in clients and proxies keys on idempotency, not on the method name.
  • Payment APIs add an idempotency key so a POST can be retried safely.

DELETE is idempotent even though the second call returns 404: the state after both calls is the same.

What do 401 and 403 mean, and how are they different?

401 means the request lacks valid credentials, so authenticate and try again; 403 means the server knows who you are and still refuses, so credentials will not help.

  • 401 should come with a WWW-Authenticate header saying how to authenticate.
  • Some APIs return 404 instead of 403 to avoid revealing that a resource exists.

The name "Unauthorized" for 401 is a historical misnomer; it means unauthenticated.

What is the difference between 502, 503 and 504?

All come from a proxy or load balancer in front of a backend: 502 means the backend gave an invalid or no response, 503 means the service is overloaded or down for maintenance and a retry later may work, and 504 means the backend did not answer within the proxy's timeout.

  • Clients retry all three with backoff, honouring Retry-After when present.
  • Health checks that count 5xx use the first digit only.

A 502 from a CDN nearly always means the origin is down; a 503 often means the CDN is shedding load to protect it.

Why did browsers open six connections per host with HTTP/1.1, and why do they not with HTTP/2?

HTTP/1.1 allows only one outstanding request per connection, so parallelism needed parallel connections; HTTP/2 multiplexes many requests as interleaved frames on one connection, so extra connections would only waste handshakes and split the congestion window.

  • The six-connection limit was a browser convention, not part of HTTP.
  • Sprites, bundling and domain sharding were workarounds for the one-at-a-time rule.

HTTP/2's remaining problem is that one lost TCP packet stalls every stream, which HTTP/3 fixes.

How does HTTP/2 differ from HTTP/1.1 on the wire?

It is binary: requests and responses are split into frames tagged with a stream ID and interleaved on one connection, headers are compressed with HPACK using a shared dictionary, and responses can arrive in any order.

  • The methods, headers and status codes are unchanged; only the framing is new.
  • Every browser requires TLS for HTTP/2.

Server push was part of the design and has since been removed from browsers in favour of Early Hints.

Explain how HTTP caching decides whether to send a request.

If a cached copy is still fresh under its Cache-Control: max-age, it is used with no request at all; if it is stale, the client sends a conditional request with If-None-Match and the ETag, and the server replies 304 with no body if unchanged or 200 with the new body if changed.

  • no-cache means always validate; no-store means never cache; private means browser only.
  • Vary tells shared caches which request headers produce different copies.

Hashed filenames with a one-year immutable max-age make the browser never ask again.

What is an ETag and when is it better than Last-Modified?

An opaque version string, usually a content hash, returned with a response and sent back in If-None-Match to ask whether the content changed; it is better than a timestamp when content can change within a second, be regenerated with the same bytes, or be served from several servers with different clocks.

  • A match returns 304 and saves the whole body.
  • Weak ETags (W/"...") mean semantically equal, not byte-identical.

Adding an ETag to an API GET is the cheapest way to make repeated polling nearly free.

Which cookie attributes should a session cookie have and why?

HttpOnly so page scripts cannot read it, Secure so it is only sent over HTTPS, and SameSite=Lax or Strict so other sites cannot make the browser send it, which blocks cross-site request forgery.

  • Max-Age controls persistence; without it the cookie dies with the browser session.
  • Path and Domain scope where it is sent.

Browsers have defaulted to SameSite=Lax since 2020, which broke some embedded widgets and fixed a class of attacks.

What is chunked transfer encoding for?

Sending a body whose total size is not known when the headers are written, as a sequence of length-prefixed chunks ending with a zero-length chunk, so the receiver still knows where the message ends without a Content-Length.

  • Used for streamed and generated responses, and by servers compressing on the fly.
  • HTTP/2 and HTTP/3 have framing built in and do not use it.

Without either mechanism the only way to end a message was to close the connection.

What does HTTPS add, and what can an observer still see?

HTTPS wraps the same HTTP in TLS, so the request and response are encrypted and the server is authenticated by a certificate whose name must match; an observer can still see the IP addresses, the packet sizes and timing, and the server name sent in the clear in the SNI extension.

  • The certificate is checked against authorities the browser trusts and against the requested name.
  • Encrypted Client Hello is the emerging fix for the SNI leak.

HSTS makes the browser refuse plain HTTP for a site it has seen the header from.

What is CORS and what is the preflight?

CORS is the set of headers by which a server tells browsers which other origins may call it from JavaScript; the preflight is an OPTIONS request the browser sends first for non-simple requests, asking whether the method and headers are allowed, before sending the real one.

  • The server answers with Access-Control-Allow-Origin and related headers.
  • It protects users of browsers, not servers; curl ignores it entirely.

A missing CORS header is the reason a working API "fails" only when called from a web page.