HTTP — Requests, Responses, Status Codes, Headers
The protocol most engineers know best. Methods, status families, the headers that actually matter, persistent connections.
What it is#
HTTP (HyperText Transfer Protocol) is the request/response protocol that powers the Web. A client opens a connection to a server (TCP by default, QUIC for HTTP/3), sends a request line plus headers plus an optional body, and reads back a status line plus headers plus an optional body. That’s it — the protocol is text-based (in HTTP/1.x), human-readable, and intentionally simple.
Over the last three decades it has accumulated three major versions (HTTP/1.1 in 1997, HTTP/2 in 2015, HTTP/3 in 2022), several mandatory headers, a catalogue of methods (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, TRACE), and a tightly defined status-code taxonomy (1xx informational, 2xx success, 3xx redirect, 4xx client error, 5xx server error). It is the protocol most engineers know best — which also means it’s the one interviewers expect you to know in detail.
HTTP is stateless by design: the server is not required to remember anything between requests. State (sessions, carts, auth) is layered on top via cookies, tokens, or server-side stores keyed by an identifier the client sends back on each request.
When to use it#
HTTP is the default application-layer protocol for almost anything client-server on the public Internet. Reach for it when:
- Web pages and APIs. Browsers speak only HTTP; REST and GraphQL both ride on it.
- Cross-organisation integration. Firewalls let port 80/443 through by default; almost nothing else.
- Request/response semantics fit. Short, bounded interactions where the client asks for something and waits for a result.
- You need an enormous ecosystem. Caching proxies, CDNs, load balancers, debugging tools, and language libraries all exist for HTTP at quality and scale that no rival has.
Reach for something else when:
- You need true bidirectional streaming. Use WebSockets (an HTTP upgrade) or gRPC (HTTP/2 streams) or raw TCP.
- You need very low latency on small messages. UDP-based protocols (QUIC, custom) avoid TCP head-of-line blocking. Note HTTP/3 already addresses this within the HTTP umbrella.
- You need fire-and-forget broadcast. UDP multicast or a pub/sub system.
- The peers are inside one process or one host. Unix domain sockets or shared memory are orders of magnitude cheaper.
How it works#
The request#
An HTTP/1.1 request is a request line, a set of header lines, a blank line, and an optional body.
GET /search?q=cats HTTP/1.1Host: example.comUser-Agent: curl/8.4.0Accept: text/htmlAccept-Encoding: gzipCookie: sid=abc123Connection: keep-aliveThe request line carries the method, path (plus query string), and HTTP version. Host became mandatory in 1.1 because one IP can host many virtual hosts — the server needs to know which site you asked for.
The response#
HTTP/1.1 200 OKDate: Sun, 17 May 2026 12:00:00 GMTContent-Type: text/html; charset=utf-8Content-Length: 1024Cache-Control: max-age=300Set-Cookie: sid=abc123; Secure; HttpOnly; SameSite=LaxConnection: keep-alive
<!doctype html>...The status line carries the version, status code, and a human-readable reason phrase. Headers describe the payload (type, length, encoding) and the cache/auth/transport semantics.
Methods and idempotency#
| Method | Safe? | Idempotent? | Body | Typical use |
|---|---|---|---|---|
| GET | yes | yes | no | retrieve a resource |
| HEAD | yes | yes | no | retrieve headers only |
| OPTIONS | yes | yes | no | discover allowed methods / CORS preflight |
| PUT | no | yes | yes | replace a resource |
| DELETE | no | yes | no/yes | remove a resource |
| POST | no | no | yes | create a resource or invoke an action |
| PATCH | no | no | yes | partially modify a resource |
Safe means “no side effects” — a caching proxy or browser pre-fetch can safely call it. Idempotent means “calling twice has the same effect as calling once” — clients and middleboxes can safely retry on timeout. POST is neither, which is why retrying a payment POST without idempotency keys is dangerous.
Status codes#
The first digit defines the family; the remaining two define the specific case.
- 1xx Informational —
100 Continue,101 Switching Protocols(used for WebSocket upgrade). - 2xx Success —
200 OK,201 Created,204 No Content,206 Partial Content(range requests). - 3xx Redirect —
301 Moved Permanently(cacheable forever),302 Found(temporary),304 Not Modified(caching),307 Temporary Redirect(preserves method),308 Permanent Redirect. - 4xx Client error —
400 Bad Request,401 Unauthorized(auth required),403 Forbidden(auth provided but insufficient),404 Not Found,409 Conflict,422 Unprocessable Entity,429 Too Many Requests. - 5xx Server error —
500 Internal Server Error,502 Bad Gateway,503 Service Unavailable,504 Gateway Timeout.
Headers that actually matter#
- Host — required since 1.1; identifies the virtual host.
- Content-Type —
application/json,text/html; charset=utf-8,multipart/form-data. Mis-set Content-Type is the most common API bug. - Content-Length / Transfer-Encoding: chunked — how the body ends.
- Cache-Control —
no-store,no-cache,max-age=N,public,private. The single most underused header. - ETag / If-None-Match — strong validators for conditional GETs.
- Authorization —
Bearer <token>,Basic base64(user:pass). - Cookie / Set-Cookie — session state; see the cookies writeup.
- Accept / Accept-Encoding / Accept-Language — content negotiation.
- CORS family —
Origin,Access-Control-Allow-Origin,Access-Control-Allow-Methods. The thing that confuses every frontend engineer once. - X-Forwarded-For / X-Real-IP / Forwarded — client IP through proxy chains.
Persistent connections#
HTTP/1.0 opened a fresh TCP connection per request — three-way handshake plus slow start every time. HTTP/1.1 made keep-alive the default: reuse the same TCP connection for a sequence of requests. Big latency win, especially with TLS (handshake costs ~1 extra RTT).
HTTP/1.1 supports pipelining (send the next request before the previous response arrives), but it suffers head-of-line blocking and is effectively disabled in all browsers. HTTP/2 fixed this by multiplexing independent streams over one TCP connection.
HTTP/2 and HTTP/3#
HTTP/1.1 — text over TCP. One request in flight at a time per connection.HTTP/2 — binary frames, stream multiplexing over one TCP connection, HPACK header compression, server push (rarely used).HTTP/3 — same semantics, runs over QUIC (UDP). Per-stream loss recovery — TCP head-of-line blocking is gone.For an interview, knowing the three versions, the connection model of each, and the head-of-line blocking story is the depth bar.
Variants#
- HTTPS — HTTP inside TLS. Same protocol; the TLS handshake adds 1–2 RTT (0 with TLS 1.3 resumption). All modern public traffic should be HTTPS.
- REST — a style of API design over HTTP: resources as URLs, methods as verbs, state in representations. Not a protocol — a convention.
- GraphQL — single endpoint, POST a query, get exactly what you asked for. Runs on HTTP but uses POST for reads.
- gRPC — Google’s RPC framework. Uses HTTP/2 streams and Protocol Buffers. High-throughput, strongly-typed, less browser-friendly.
- WebSocket — opens via HTTP upgrade (
Upgrade: websocket), then drops out of HTTP semantics to carry arbitrary bidirectional frames. - Server-Sent Events (SSE) — one-way streaming from server to client over a long-lived HTTP response. Simpler than WebSocket for fan-out-style updates.
- WebDAV / CalDAV / CardDAV — extensions adding distributed-authoring methods (PROPFIND, MKCOL). Mostly file servers and calendars.
Trade-offs#
Other trade-offs:
- Stateless protocol, stateful app — the protocol gives you nothing for free, but lets you scale horizontally (any server can answer any request, with session lookup as the cost).
- Text vs binary — text (1.1) is debuggable with telnet/curl. Binary (2, 3) is faster to parse and compress but requires real tooling to inspect.
- Versioning — almost all servers and clients still speak 1.1. Don’t assume 2/3 — fall back gracefully.
- Idempotency under retry — POSTs without idempotency keys silently duplicate work on retry. Add an
Idempotency-Keyheader for any state-changing call worth retrying.
Common pitfalls#
- Confusing 401 and 403. Already covered — but worth restating. Wrong codes mislead clients and break retry logic.
- Forgetting the Host header. Almost impossible with libraries; happens with raw socket implementations. Without it, the server can’t pick the right vhost.
- Caching POST responses. HTTP allows it (RFC 9110) but virtually nothing implements it. Cache GETs.
- Trusting
X-Forwarded-Forfrom untrusted hops. Clients can spoof it. Only trust the rightmost trusted-proxy chain. - Re-using one TCP connection across origins with HTTP/2. Connection coalescing is per-authority and depends on cert SAN coverage — surprising bugs when certs change.
- Ignoring Content-Length on streamed responses. Stream with
Transfer-Encoding: chunkedor set Content-Length precisely; mismatches kill keep-alive. - Not setting cache headers. Without
Cache-Control, intermediate proxies guess —max-age=300is a safe default for static-ish JSON;no-storefor sensitive responses. - Body on GET. RFC 9110 says the body has no defined semantics on GET — many proxies and CDNs strip it. Don’t.
Why HTTP defaulted to TCP, and what changed
HTTP was designed in 1989-91 over TCP because TCP was the obvious reliable transport. By the 2000s, the TCP connection lifecycle (3-way handshake plus slow start) was the dominant overhead for short web fetches. HTTP/2 partly mitigated by multiplexing on one TCP connection — but TCP’s in-order delivery means one lost packet blocks every stream. HTTP/3 over QUIC pushes reliability into per-stream user-space code, side-stepping TCP entirely. QUIC also folds the TLS handshake into the connection setup, saving an RTT.
Related building blocks#