HTTP protocol
What an HTTP request and response look like
An HTTP exchange is text with a fixed anatomy: a request line (method, path, version), then headers, a blank line, and an optional body. The response mirrors it, starting with a status line:
GET /api/orders?page=2 HTTP/1.1
Host: shop.example
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
[{"id": 41}, {"id": 42}]
HTTP is stateless: the server keeps no memory of previous requests, so anything contextual (who you are, what session) must ride along in headers on every single request.
Safe and idempotent HTTP methods
The method names an action. It also makes a contract that the whole web infrastructure relies on:
- Safe. GET, HEAD, OPTIONS: read-only, no server state changes. Because of this promise, browsers prefetch links, crawlers follow them, and caches store the results without asking.
- Idempotent. All safe methods plus PUT and DELETE: doing it N times leaves the same state as doing it once. DELETE twice = still deleted. This promise is what makes blind retries legal: a client or proxy that hits a timeout can resend without asking anyone.
- Neither. POST and PATCH. Retrying a POST can create a second order or a second charge, which is why browsers show the "resubmit form?" warning.
❌ "POST is more secure than GET because the data is hidden in the body." Without TLS, URL and body are equally plaintext; with TLS, both are encrypted. Keeping secrets out of URLs still matters (they end up in logs, history, bookmarks), but that is exposure hygiene, not protocol security. Pick the method for its semantics; where the payload sits has no bearing on security.
How to read HTTP status codes
The first digit routes the blame: 2xx means it worked, 3xx means look elsewhere, 4xx means your request is wrong and you should fix it before retrying, 5xx means the server failed and a retry may succeed. 1xx are interim and rare in app code.
The codes people get wrong in interviews:
- 401 vs 403. 401 means unauthenticated: "I don't know who you are, prove it" (it ships a challenge). 403 means authenticated but denied: "I know exactly who you are, and the answer is no." Sending 403 when you mean 401 breaks clients that would otherwise trigger a login flow.
- 204 No Content. Success with no body on purpose, and the right answer for a successful DELETE. Don't return 200 with an empty JSON husk.
- 429 Too Many Requests. Rate limited. A well-behaved API pairs it with
Retry-Afterso clients back off instead of hammering. - 404 vs 410. 404 is "not found (maybe ever, maybe just now)". 410 is an explicit "gone forever, delete your links".
HTTP/1.1 keep-alive and the head-of-line blocking problem
HTTP/1.0 opened a fresh TCP connection per request: a full handshake every time, and TCP never got past its cold slow-start phase. HTTP/1.1's headline fix was persistent connections (keep-alive, on by default), plus the Host header (many sites, one IP) and chunked transfer.
But 1.1 kept one hard rule: one response at a time per connection. Request B waits until response A finishes. This is head-of-line blocking at the application layer. Pipelining (send B before A returns) was specced and abandoned: responses still had to arrive in order, and intermediary proxies mishandled it; browsers shipped it disabled. The ecosystem's workaround was brute force: ~6 parallel connections per origin, domain sharding to get more, sprites and bundles to need fewer requests. All of these are anti-patterns now.
How HTTP/2 multiplexing works
h2 replaces the text protocol with binary framing. Requests and responses become streams, chopped into frames that interleave freely over one TCP connection:
HTTP/1.1: [====== response A ======][=== B ===] ← B queued behind A
HTTP/2: [A][B][A][A][B][A][B] ← frames interleaved, one connection
A slow response no longer blocks a fast one, and sharding and bundling lose their reason to exist. Two companion features:
- HPACK. Header compression via shared static and dynamic tables: repeated headers (cookies, user-agent) become small index references. It is a purpose-built scheme because naive gzip-over-TLS enabled the CRIME attack.
- Server push. Let servers send resources before being asked, and died in practice: the server can't see the client's cache, so it kept pushing bytes the client already had.
<link rel="preload">and 103 Early Hints won. ❌ Citing push as a current h2 benefit dates your knowledge.
One caveat matters for HTTP/3: h2 removed HoL blocking at the HTTP layer only. All those streams still share a single TCP pipe, and TCP has opinions.
301 vs 302 vs 307 vs 308 redirects
Two questions pick the code: is the move permanent, and must the method survive the redirect?
| may rewrite POST→GET | must preserve method+body | |
|---|---|---|
| temporary | 302 | 307 |
| permanent | 301 | 308 |
❌ "301 and 302 are basically the same." They differ in permanence (301 is cached aggressively; browsers may never re-check), and both share the legacy flaw: a redirected POST may arrive as a bodyless GET. If the client must replay the method, only 307/308 guarantee it. 303 See Other is the designed opposite ("the result lives over there, GET it") and the backbone of the POST-redirect-GET pattern that kills double-submits.
How content negotiation and Vary work
One URL can serve several representations: JSON or HTML, English or Ukrainian, brotli or plain. The client states ranked preferences with q-values; the server picks and labels its choice:
Accept-Language: uk, en;q=0.8
Accept-Encoding: br, gzip;q=0.9
HTTP/1.1 200 OK
Content-Language: uk
Content-Encoding: br
Vary: Accept-Language, Accept-Encoding
Vary declares which request headers shaped the answer, so caches store separate variants instead of serving Ukrainian brotli to an English gzip client (the cache-key mechanics live in the caching topic). Skipping Vary on a negotiated response is the classic silent bug.
Why HTTP/3 runs on QUIC instead of TCP
h2 put every stream into one TCP connection, and TCP guarantees in-order delivery of one byte stream. Lose a single packet and TCP holds back everything behind it until retransmission, even bytes belonging to streams that lost nothing. Head-of-line blocking didn't disappear in h2; it moved down a layer, and on lossy networks h2 can underperform h1's six naive connections.
TCP couldn't be fixed in place: it lives in kernels and in decades of middleboxes that drop anything unfamiliar (ossification). So QUIC rebuilds transport in user space on top of UDP:
- Streams are first-class at the transport. Each stream recovers from loss independently; a lost packet stalls only its own stream.
- One handshake instead of two. Transport and TLS 1.3 setup merge: 1 RTT cold, 0-RTT on resumption, where the first request rides along with the handshake. Caveat: 0-RTT data is replayable, so it's only safe for idempotent requests.
- Connection migration. Connections are identified by a connection ID rather than the IP/port 4-tuple. Walk out of Wi-Fi onto cellular and the connection survives instead of resetting.
Semantics did not change. Methods, status codes, and headers mean the same on 1.1, 2, and 3; versions renegotiate the wire and leave the meaning alone.
When GraphQL beats REST and what it costs
REST's failure modes are fetch-shaped: over-fetching (fixed representations carry fields you don't need) and under-fetching (one page = N sequential calls because responses reveal the next URLs). GraphQL inverts it: one typed schema, the client names exactly the fields and relationships it wants, one round trip.
What you pay:
- HTTP caching regression. Queries usually go as POST to a single endpoint, so everything keyed on method+URL (browser cache, CDN) goes blind. Persisted queries (send a hash of a pre-registered query, via GET) claw back cacheability and kill arbitrary-query abuse in one move.
- N+1 moved, not removed. One query fans out to resolvers; a naive server hits the database once per list item. DataLoader-style batching is table stakes.
- Schema ownership. Types, resolvers, versioning-by-deprecation, query-cost limits: GraphQL earns its keep when many clients need many shapes and the schema works as a contract between teams. For one known client, a BFF or aggregation endpoint (one cacheable GET shaped for that page) buys the same round-trip win for a fraction of the machinery.
❌ "We're on HTTP/2, request count is free now." Multiplexing makes parallel requests cheap. It does nothing for dependency waterfalls (no protocol can request data the client doesn't yet know it needs), nothing for 40× auth/serialization on the server, and nothing for payload waste. Diagnose which one is slow before prescribing.
The judgment calls interviewers actually probe
- A proxy retried a payment POST; a customer was charged twice. Nothing malfunctioned. Retrying is legal precisely because idempotent methods promise convergence, and POST never made that promise. Fix the API: an
Idempotency-Keyheader the server deduplicates on, so the retry converges by construction. - "Would you adopt GraphQL here?" The honest answer states the costs (caching, resolvers, schema ownership) and a condition under which each side wins, without committing to a blanket verdict.
- "What did each protocol hop buy?" 1.1: connection reuse. 2: the application-layer queue. 3: the transport-layer queue, plus handshake latency and migration. Across the three versions, head-of-line blocking moved down the stack until QUIC removed it at the transport.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.