HTTP/1.1
HTTP/1.1 is the model of the web that everyone still carries in their head, even on sites that long since moved to HTTP/2 or /3. A request is a text request line — a method, a path, and a version — followed by headers, a blank line, and an optional body. A response is a status line with a numeric code, its own headers, and a body. That is the entire protocol on the wire, and you can speak it by hand over a raw socket.
What matters is that the semantics defined here — methods, status code classes, caching and conditional headers — did not change in HTTP/2 or HTTP/3. Those versions rewrote the wire format; the meaning of GET, of 304 Not Modified, of ETag and Cache-Control, is identical across all three. Learn it once here. The newer versions are just faster envelopes around the same letters.
Methods, Safety, and Idempotency
The common methods carry two orthogonal properties. A method is safe if it only reads — GET, HEAD, and OPTIONS must not change server state, which is why a crawler can fetch them freely. A method is idempotent if making the same call twice has the same effect as making it once. GET, PUT, and DELETE are idempotent: PUT of the same resource twice leaves it in one state, and DELETE twice still ends with it gone.
POST is neither safe nor idempotent — two POSTs of the same form create two orders. This distinction is not academic: it decides what is safe to retry. A client, proxy, or load balancer can transparently retry an idempotent request after a timeout without risk, but retrying a POST can double-charge a customer. PATCH is the odd one out — it is not guaranteed idempotent, because "apply this delta" can compound, which is why APIs that want safe retries prefer PUT with a full representation.
Status Code Classes
Status codes group into five classes by their first digit, and the class tells you who owns the problem. The 1xx informational class — 100 Continue, 101 Switching Protocols, 103 Early Hints — is rare in everyday code; the four you handle constantly are the rest. 2xx is success — 200 OK, 201 Created, 204 No Content. 3xx is redirection or a cache decision — 301/302 move you, and 304 Not Modified tells the client its cached copy is still good and ships no body. 4xx means the client got it wrong: 404 Not Found, 401 Unauthorized, 403 Forbidden, and 429 Too Many Requests.
5xx means the server (or something acting as it) failed: 500 Internal Server Error, and the proxy trio 502/503/504 covered below. The ones that actually bite in production are predictable. 304 is the one that saves bandwidth and is silently broken when caching headers are wrong. 429 is the one whose Retry-After header you must honor or you hammer a struggling service. And treating a 4xx as a server fault — paging an on-call engineer for a flood of 404s that are really a bad client — wastes nights.
# curl -v shows the request line, headers, and the status line curl -v https://example.com/api/users/42 > GET /api/users/42 HTTP/1.1 <- method, path, version > Host: example.com <- required in HTTP/1.1 > Accept: application/json < HTTP/1.1 200 OK <- status line: code + reason < Content-Type: application/json < ETag: "a1b2c3" <- validator for conditional GET < Cache-Control: max-age=60
Content Negotiation, Caching, and Conditional Requests
Headers do the protocol's real work. Content negotiation lets the client state preferences — Accept: application/json, Accept-Encoding: gzip, br, Accept-Language — and the server picks a representation and echoes its choice in Content-Type and Content-Encoding. Caching is driven by Cache-Control: max-age=60 says a response is fresh for 60 seconds, no-store forbids caching entirely, and private restricts it to the browser, not shared proxies.
Conditional requests are the bandwidth saver. The server tags a response with an ETag (a content hash) or Last-Modified date. On the next fetch the client sends If-None-Match: "a1b2c3" or If-Modified-Since; if nothing changed the server replies 304 Not Modified with headers but no body, and the client reuses its cached copy. A megabyte image revalidates in a few hundred bytes — that is the entire point of 304, and it collapses to full re-downloads the moment your ETag or Vary headers are misconfigured.
Persistent Connections and Head-of-Line Blocking
HTTP/1.0 opened a fresh TCP connection per request and paid the handshake every time. HTTP/1.1 made keep-alive the default: the connection stays open and carries request after request, amortizing the TCP and TLS setup across many fetches. This is a large win — a page with 40 assets reuses a handful of connections instead of opening 40 — but it has a hard ceiling built into the protocol.
On a single HTTP/1.1 connection, responses must come back in request order. Pipelining tried to let the client fire several requests without waiting, but the server still has to answer them sequentially, so one slow response blocks every request queued behind it — classic head-of-line blocking at the application layer. Browsers gave up on pipelining and instead opened roughly six parallel connections per host to get concurrency. That workaround — six connections, each with its own HoL problem — is exactly what HTTP/2's multiplexing replaced.
502 Bad Gateway means a proxy reached the upstream but got an invalid or broken reply — the backend answered with garbage, closed the connection mid-response, or crashed. The upstream is up but malfunctioning. Look at the backend's own logs and its response, not the network path.
503 Service Unavailable means the server itself is deliberately refusing — overloaded, in maintenance, or out of capacity — and usually sends a Retry-After. Nothing is broken; there is simply no instance willing to serve right now. Scale out or wait. 504 Gateway Timeout means the proxy reached the upstream but the upstream never replied in time — the backend is hung, slow, or the connection silently dropped. Chase backend latency and timeout settings, not bad output.
- Retrying a non-idempotent POST or PATCH after a timeout the same way you would a GET. The first call may have succeeded before the timeout fired, so the retry creates a duplicate order or double-charge with no error to warn you.
- Treating
4xxresponses as server faults and alerting on them. A flood of404or400means clients are sending bad requests; paging an engineer for the server's correct rejection burns on-call time on a non-incident. - Ignoring the
Retry-Afteron a429and retrying immediately. The server is explicitly rate-limiting you; hammering it back without backoff turns a soft throttle into a hard outage for everyone behind that service. - Misreading HTTP/1.1 head-of-line blocking as server slowness. One slow response stalls every request queued behind it on the same connection; the server may be fine, and you waste hours profiling a backend whose real problem is connection serialization.
- Setting
Cache-Controlwithout a validator, or gettingVarywrong, so conditional requests never produce a304. Every revalidation re-downloads the full body, quietly multiplying bandwidth and latency on assets that never changed.
- Map operations to methods by their idempotency: use PUT with a full representation for writes you want to retry safely, and reserve POST for genuinely non-idempotent creates so clients know what is safe to repeat.
- Send
ETagorLast-Modifiedon cacheable responses and honorIf-None-Match/If-Modified-Sincewith a304, so unchanged resources revalidate in a few hundred bytes instead of a full re-download. - Honor
Retry-Afteron429and503and back off exponentially, so a throttled or overloaded service gets room to recover instead of a retry storm. - Distinguish the
5xxtrio in monitoring — alert on502/504as backend health problems and treat503as a capacity signal — so the dashboard points at the right subsystem. - Add idempotency keys to write endpoints (a client-supplied unique header the server deduplicates on) so a retried POST after a timeout is safe even though the method itself is not idempotent.
Knowledge Check
Why is it safe for a load balancer to transparently retry a timed-out PUT but risky to retry a POST?
- PUT is idempotent, so repeating it yields the same state, whereas a repeated POST can create a duplicate
- PUT sends a smaller body, so the retry costs less bandwidth than a POST
- PUT is a safe method that never changes state, so retrying it does nothing
- PUT responses are automatically versioned by the protocol, so the server transparently ignores every duplicate retry call
A proxy returns 504 Gateway Timeout for some requests to a backend. What does that point at?
- The upstream was reached but never replied in time — backend latency or a hung request
- The upstream replied with an invalid or broken response that the front proxy simply could not parse correctly
- The server deliberately refused because it was overloaded or in maintenance
- The client sent a malformed request the backend rejected
What does a conditional request with If-None-Match save when the resource is unchanged?
- The response body — the server returns
304with headers only, and the client reuses its cache - The entire TCP and TLS handshake round trip, because the underlying connection is skipped over completely
- All server processing, since the request never reaches the application
- The body is gzip-compressed instead of sent in full
You got correct