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.
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.
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>...
- 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. - Headers: one
Name: valueper line. Names are case-insensitive.Hostis the only one HTTP/1.1 requires, because one server address may host many sites and this is how it picks. - A blank line ends the headers. If there is a body,
Content-Lengthsays how many bytes follow, orTransfer-Encoding: chunkedsays 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. - 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
curl -v https://example.comprints 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: valueline 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.
| Method | Means | Safe | Idempotent | Body |
|---|---|---|---|---|
GET | Give me this. The one caches understand. | Yes | Yes | No |
HEAD | Like GET but only the headers. Checking size or freshness. | Yes | Yes | No |
POST | Here is something: create it, process it, act on it. | No | No | Yes |
PUT | Replace what is at this path with this body. | No | Yes | Yes |
PATCH | Change part of what is at this path. | No | No | Yes |
DELETE | Remove what is at this path. | No | Yes | Rarely |
OPTIONS | What may I do here? Used by browsers before cross-site requests. | Yes | Yes | No |
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.
| Code | Means | Notes |
|---|---|---|
200 OK | Here is what you asked for | The body is the answer |
201 Created | Your POST made something | Usually with a Location header pointing at it |
204 No Content | Done, nothing to return | Common for DELETE |
301 / 308 | Moved permanently | Browsers and search engines remember it. 308 keeps the method |
302 / 307 | Moved for now | Come back to the original next time. 307 keeps the method |
304 Not Modified | Your cached copy is still good | No body. The point of ETags |
400 Bad Request | The request itself is malformed | Do not retry without changing it |
401 Unauthorized | Who are you? Credentials missing or wrong | Really means "unauthenticated" |
403 Forbidden | I know who you are and no | Credentials will not help |
404 Not Found | Nothing at this path | Also used to hide things that exist |
429 Too Many Requests | Slow down | With Retry-After. Rate limiting |
500 Internal Server Error | The server broke | A bug or crash on their side |
502 Bad Gateway | The proxy could not get a valid answer from the backend | The backend is down or misbehaving |
503 Service Unavailable | Overloaded or in maintenance | Retry later. Often with Retry-After |
504 Gateway Timeout | The backend took too long | The 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.
- REST APIs are the method table applied to resources:
GET /users/42,PUT /users/42,DELETE /users/42,POST /usersto 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.
| Job | Header | What it does |
|---|---|---|
| Routing | Host | Which site, when one address serves many. Required in every request. |
X-Forwarded-For, Forwarded | Added by proxies so the backend can see the real client address. | |
| The body | Content-Type | What the body is: text/html, application/json, image/png, multipart/form-data. |
Content-Length | How many bytes, so the receiver knows where the message ends. | |
Transfer-Encoding: chunked | Body sent in length-prefixed pieces, for when the size is not known up front. | |
| Negotiation | Accept | What the client can handle: application/json, image/webp. |
Accept-Encoding / Content-Encoding | Client can take gzip, br; server says the body is compressed with one of them. | |
Accept-Language | Preferred languages, for sites that translate. | |
| Identity | Authorization | Credentials: Bearer token, or Basic base64(user:pass). |
Cookie / Set-Cookie | The client returns what the server gave it earlier. | |
User-Agent | What software is asking. Widely lied about. | |
| Caching | Cache-Control | How long and by whom the response may be cached. |
ETag / If-None-Match | A version tag and a conditional request that returns 304 if unchanged. | |
Vary | Which request headers change the response, so caches keep separate copies. | |
| Control | Location | Where to go, on a redirect or after a 201. |
Retry-After | Seconds 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.
- nginx and Cloudflare add
X-Forwarded-Foron 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; theOPTIONSpreflight 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"]
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.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Wire format | Text | Binary frames | Binary frames |
| Requests in flight per connection | 1 | Many (typically 100) | Many |
| Connections a browser opens per host | About 6 | 1 | 1 |
| Header compression | None | HPACK | QPACK |
| One lost packet stalls | That connection's current request | All streams on the connection | One stream |
| Transport | TCP | TCP | QUIC over UDP |
| Encryption | Optional (https) | Required by every browser | Always |
- Most websites serve HTTP/2, and the largest serve HTTP/3; devtools' protocol column shows
h2orh3. - 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.
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"]
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.
- Hashed asset names like
app.3f9a1c.jswithmax-age=31536000, immutableare 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-ageruns out; without a purge, a longmax-ageon HTML means users see stale pages. stale-while-revalidatelets 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 anETagis 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
ETagorLast-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.
- Every login on the web is a
Set-Cookieafter a successful POST; logging out is aSet-CookiewithMax-Age=0. - Browsers default
SameSitetoLaxsince 2020, which quietly fixed a class of CSRF attacks and broke some cross-site embedded widgets. - Cloud APIs take
Authorization: Bearerwith a short-lived token, refreshed by the client, so a leaked token is only useful for minutes.
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
Authorizationheader; whoever holds it is authenticated. - CSRF
- Cross-site request forgery: another site making the browser send a request with the user's cookies.
SameSiteblocks 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.
- 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 sayshttp://. - 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.
Hostroutes,Content-TypeandContent-Lengthdescribe the body,Accept-Encodingnegotiates compression,AuthorizationandCookieidentify,Cache-ControlandETagcache.- 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 withimmutableare the standard pattern. - Cookies need
HttpOnly,SecureandSameSite. APIs use bearer tokens inAuthorization. - 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.
Hostis the only required request header in HTTP/1.1.- The body's end is marked by
Content-Lengthor 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-Authenticateheader 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-Afterwhen 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-cachemeans always validate;no-storemeans never cache;privatemeans browser only.Varytells 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-Agecontrols persistence; without it the cookie dies with the browser session.PathandDomainscope 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-Originand 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.