Why HTTP exists
Two machines that have never met need to agree on how to ask for something and how to answer. HTTP is that agreement: a text-based, request/response, stateless application protocol. Every message is self-describing — the method says the intent, the path says the resource, the headers say the metadata, the body carries the payload.
The design choices that made it win are worth naming. It is stateless, so any server in a fleet can answer any request and you can scale horizontally by adding boxes. It is uniform, so caches, proxies and load balancers can act on a message without understanding your application. It is text-based (until HTTP/2), so it was trivially debuggable in an era with no tooling.
The full journey of one request
- URL parsing. The client splits scheme, host, port, path, query and fragment. The fragment never leaves the browser.
- DNS resolution.
api.example.combecomes an IP, usually from the OS or browser cache rather than an actual network round trip. - TCP handshake. SYN, SYN-ACK, ACK — one round trip before a byte of your data moves. QUIC (HTTP/3) folds this into the crypto handshake.
- TLS handshake. Certificate validation, key exchange, ALPN negotiation (which is how the client and server agree on HTTP/1.1 vs HTTP/2).
- Request sent. Request line, headers, blank line, optional body.
- Server-side work. Reverse proxy → app server → auth → handler → database → serialisation. This is usually where your latency lives.
- Response returned. Status line, headers, body — possibly streamed in chunks.
- Connection reuse. The socket stays open for the next request instead of paying handshakes again.
Anatomy of a request
POST /v1/orders?dryRun=false HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 27
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Idempotency-Key: 7c9e-41ab-9f01
{"sku":"AB-19","qty":2}- Method — the intent (GET, POST, PUT, PATCH, DELETE…). Covered in depth in the methods article.
- Path — identifies the resource. Hierarchical, server-side routing key.
- Query string — non-hierarchical modifiers: filtering, sorting, pagination.
- Version — HTTP/1.1 here; HTTP/2 and /3 send the same semantics in binary frames.
The headers that actually matter
| Header | Direction | What it decides |
|---|---|---|
| Host | Request | Which virtual host / site on a shared IP answers |
| Content-Type | Both | How to parse the body — JSON, form, multipart |
| Accept | Request | Content negotiation: what the client can handle |
| Authorization | Request | Bearer token, Basic, or a signed scheme |
| Cache-Control | Both | Whether and how long anything may cache this |
| ETag / If-None-Match | Both | Conditional GET → cheap 304 responses |
| Content-Encoding | Response | gzip / br compression of the body |
| Set-Cookie / Cookie | Both | Session continuity across stateless requests |
| X-Request-Id | Both | Correlating one user action across many services |
Header names are case-insensitive. Order is not significant. Duplicated headers are legal and combined with commas — which is a real source of bugs when a proxy appends to X-Forwarded-For and your code reads only the first value.
The response and status codes
HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/orders/8812
Cache-Control: no-store
{"id":8812,"status":"pending"}| Class | Meaning | Ones you will actually use |
|---|---|---|
| 1xx | Informational | 101 Switching Protocols (WebSocket upgrade) |
| 2xx | Success | 200 OK, 201 Created, 202 Accepted, 204 No Content |
| 3xx | Redirection | 301 permanent, 302/307 temporary, 304 Not Modified |
| 4xx | Client error | 400, 401 unauthenticated, 403 unauthorised, 404, 409 conflict, 422, 429 rate limited |
| 5xx | Server error | 500, 502 bad gateway, 503 unavailable, 504 timeout |
Statelessness, cookies and tokens
HTTP itself remembers nothing between requests. Every request must carry everything the server needs. Sessions are built on top of that: the server issues an opaque cookie or a signed token, and the client replays it each time.
- Cookies — automatic, sent by the browser, vulnerable to CSRF unless you set
SameSite; useHttpOnlyandSecurealways. - Bearer tokens — explicit, immune to CSRF by default, but must be stored somewhere and are hard to revoke before expiry.
Statelessness is what allows a load balancer to send consecutive requests from one user to different servers. The moment you keep session data in a process's memory you have quietly reintroduced sticky sessions and broken horizontal scaling.
Connections: keep-alive, HTTP/2, HTTP/3
| Version | Transport | Key property | Main limitation |
|---|---|---|---|
| HTTP/1.1 | TCP | Keep-alive, one request at a time per connection | Head-of-line blocking; browsers open ~6 connections per host |
| HTTP/2 | TCP + TLS | Binary framing, multiplexed streams, header compression (HPACK), server push | TCP-level head-of-line blocking on packet loss |
| HTTP/3 | QUIC over UDP | Independent streams, 0-RTT resumption, connection migration across networks | UDP sometimes blocked; newer tooling |
The semantics never changed. A GET is a GET in all three. What changed is framing and transport — which is why sprite sheets and domain sharding, sensible under HTTP/1.1, became actively harmful under HTTP/2.
Caching and conditional requests
GET /v1/products/19 HTTP/1.1 If-None-Match: "a3f19c" HTTP/1.1 304 Not Modified ETag: "a3f19c" Cache-Control: max-age=60, stale-while-revalidate=300
max-age— how long a fresh copy may be served without asking.s-maxage— the same, but only for shared caches like a CDN.no-cache— cache it, but revalidate before every use. Not the same asno-store.no-store— never write it anywhere. Use for anything authenticated and personal.Vary— the header set that partitions the cache key. ForgettingVary: Authorizationis how one user's data reaches another.
How to debug a request properly
curl -v -X POST https://api.example.com/v1/orders \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer $TOKEN' \
-d '{"sku":"AB-19","qty":2}' \
-w '\ndns=%{time_namelookup} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n'Those timings split the problem instantly. High time_namelookup is DNS. High time_appconnect is TLS or a distant region. High time_starttransferwith a low total is server-side work. A slow total with fast TTFB is a large or slowly streamed body.
Interview framing
“HTTP is a stateless request/response protocol. A cold request pays DNS, TCP and TLS round trips before any application work, so we pool connections and keep HTTP/2 to a single origin. Reads are cacheable with ETags and stale-while-revalidate at the CDN; writes are no-store and carry an idempotency key so retries after a 504 are safe. Auth is a bearer token rather than a server-side session so any instance can serve any request.”