Draw HTTP the way most people first learn it and you get one arrow going out and one coming back:
Client → Request → Server
Client ← Response ← Server
That's not wrong, exactly, it's just incomplete enough to be misleading. A real page load involves resolving DNS, establishing a connection, negotiating TLS, sending the request, receiving the response, checking whether any of it could've been served from cache, and usually fetching several more resources the same way. Understanding HTTP well has less to do with memorizing status codes and more to do with understanding why the protocol is shaped the way it is, because most of those shapes are deliberate answers to "how do we make this work at the scale of the entire web."
I put together an interactive breakdown of the whole thing on SeeItFlow, if you'd like to see it laid out visually. Here's the written version.
The actual contract underneath everything
Strip away every feature built on top of it, and HTTP is one exchange. The client sends a request:
GET /products/42 HTTP/1.1
Host: example.com
Accept: application/json
Authorization: Bearer ...
and the server sends back a response:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=300
{ "id": 42, "name": "Keyboard" }
A method, a target, headers, optionally a body, going one direction. A status code, headers, optionally a body, coming back. That's genuinely the whole protocol at its foundation. Cookies, authentication, caching, compression, content negotiation, all of it is built on top of this one request/response shape, not bolted on as something separate.
Why statelessness is a feature, not a gap
One property of HTTP surprises people the first time they think about it carefully: the server doesn't automatically know that this request came from the same person as the last one. Every request stands alone. If an application wants to recognize a returning user, it has to build that itself, a cookie, a session ID, a JWT, a record in a database or a Redis store keyed on some token.
That sounds like something HTTP is missing. It's actually a large part of why HTTP-based systems scale as well as they do. Picture three application servers behind a load balancer:
┌── Server A
Client → LB ────┼── Server B
└── Server C
If each server kept a user's session in its own local memory, the load balancer would need to remember which server that user landed on and keep routing them back there, sticky sessions. Lose that server and you lose the session with it. Keep the state external instead, in a cookie the client carries or a store every server can read, and any healthy server can answer the next request. That's what makes horizontal scaling and failover straightforward instead of a coordination problem.
The method is a promise the rest of the internet relies on
GET, POST, PUT, PATCH, DELETE aren't just different names for "do something," picked by convention. Each one tells every piece of infrastructure between the client and the server what kind of operation this is, and that infrastructure acts on it without checking with you first.
GET means read, and more specifically it means safe, nothing changes as a result. That's exactly why browsers prefetch links, why crawlers follow them automatically, why caches and proxies feel entitled to store the response and serve it to someone else. An endpoint like:
GET /delete-account
breaks every one of those assumptions at once. A crawler indexing the site can trigger a deletion just by following a link, because it was told GET requests are safe to follow, and this one wasn't. That's not a bug in the crawler, it's the endpoint lying about what kind of request it is.
Status codes are instructions, not just labels
The useful grouping is 2xx for success, 3xx for redirects and cache validation, 4xx for something wrong with the request itself, 5xx for something that broke on the server's or an upstream's side while handling it:
200 OK 301 Moved Permanently 400 Bad Request 500 Internal Server Error
201 Created 304 Not Modified 401 Unauthorized 502 Bad Gateway
204 No Content 403 Forbidden 503 Service Unavailable
404 Not Found 504 Gateway Timeout
429 Too Many Requests
That grouping matters most when deciding what's worth retrying. A 400 or 401 will come back identical no matter how many times the exact same request is resent, so retrying just wastes a round trip confirming what you already knew. A 503 or 502 often represents something genuinely temporary, a struggling dependency, a proxy that couldn't reach its upstream in time, and retrying with backoff can actually succeed. Treating these two categories the same is how naive retry logic turns a brief hiccup into wasted load on an endpoint that was never going to say yes.
Caching: the fastest request is the one you skip
A response header like:
Cache-Control: max-age=3600
tells the browser it can reuse this exact response for an hour without asking again. When freshness needs checking without re-downloading the whole payload, ETags handle that cheaply:
If-None-Match: "abc123"
and if nothing changed, the server replies 304 Not Modified with no body at all, just confirmation that what's already cached is still good.
Production systems take this further with content-hashed filenames, app.a1b2c3.js, styles.8f91de.css, so those assets can be cached for close to forever, a new deployment produces a new filename rather than overwriting the old content under the same one. The HTML entry point is the one piece kept revalidated, so a new deploy takes effect immediately: fresh HTML references the new hashed filenames, and everything downstream of that can be cached as aggressively as you like.
HTTPS is HTTP, not something separate from it
HTTPS isn't a competing protocol, it's HTTP carried inside a TLS-encrypted connection. TLS adds three properties HTTP alone doesn't have on its own: confidentiality, so nobody watching the network can read the traffic, integrity, so nobody can modify it in transit undetected, and authentication, so the client has a real way to verify which server it's actually talking to.
That gives you an ordering: DNS resolves, a connection gets established, TLS negotiates, and only then does the HTTP request actually go out.
DNS → Connection → TLS handshake → HTTP request → HTTP response
Every step there is a round trip happening before your application code ever sees the request, which is exactly why connection setup and handshake latency matter as much as they do for how fast a page feels.
Why HTTP/2 and HTTP/3 exist
HTTP/1.1 worked well for a long time, but a modern page pulls in dozens of resources, HTML, CSS, several JS bundles, API calls, images, and browsers historically worked around HTTP/1.1's limits by opening several parallel connections just to fetch more of them at once.
HTTP/2 addressed the actual bottleneck with multiplexing: many requests share a single TCP connection.
One TCP connection
├── Stream 1 → HTML
├── Stream 3 → CSS
├── Stream 5 → JavaScript
└── Stream 7 → API response
The catch is that HTTP/2 still rides on TCP, and TCP guarantees ordered delivery, so if one packet gets lost, every stream sharing that connection waits for it to be resent, even streams that had nothing to do with the lost packet.
HTTP/3 solves that specific issue by moving off TCP and onto QUIC, which runs over UDP and gives each stream independent delivery. Lose a packet on one stream and only that stream stalls, the rest keep moving. The short version: HTTP/2 multiplexes requests over one connection, HTTP/3 keeps that multiplexing while getting rid of the head-of-line blocking TCP still imposes underneath it.
What actually sits between a browser and your application
A request rarely travels straight from a browser to an application server. A more realistic path looks like:
User
↓
CDN / Edge
↓
Reverse Proxy / Load Balancer
↓
Application Servers
↓
Cache / Database / Other Services
The CDN serves cached content from somewhere physically close to the user and often terminates TLS right there at the edge. A reverse proxy handles routing, TLS termination further in, buffering, compression, rate limiting. A load balancer spreads requests across whichever application instances are healthy right now. And because the application servers stay stateless, adding or removing instances doesn't require coordinating anyone's in-flight session, the statelessness property from earlier showing up again, one layer further out.
Where this connects to everything else
Once HTTP is running at real scale, most of the interesting problems aren't about HTTP syntax at all, they're things like connection pool exhaustion, cache stampedes, retry storms, a slow upstream dragging down an otherwise healthy service, or non-idempotent operations getting retried into duplicate side effects. A payment request can succeed on the server while the response itself gets lost on the way back, the client sees a timeout, retries, and if that operation isn't idempotent, the retry charges the customer a second time for something that already worked. That single failure mode is why production HTTP design ends up connected directly to idempotency keys, exponential backoff, jitter, rate limiting, caching, and having enough observability to tell these situations apart.
The takeaway
HTTP looking simple from the outside is exactly why it's worth understanding what's underneath: statelessness that enables scaling, methods that are promises other systems rely on, status codes that carry instructions rather than just outcomes, caching that eliminates requests before they happen, and a whole layer of proxies and CDNs doing real work before a request ever reaches your code. None of that shows up in the one-arrow diagram, and all of it is what actually makes the web work at its current scale.
References
I kept this one focused on the request path itself. The fuller guide on SeeItFlow goes further into cookies and sessions, browser caching in more depth, and HTTPS/TLS handshakes step by step. There's also a dedicated production engineering walkthrough covering CDNs, reverse proxies, and load balancing, and an engineering insights guide focused on production failure patterns.
Top comments (0)