How HTTP Works: What Happens After DNS Hands You an IP

1. Why This Topic Matters
The last post ended on a promise: once DNS resolves a name to an IP, what actually happens next? Every post in this series up to now — TCP, UDP, and finally DNS — has been about getting to a server. None of them explain what the client and server actually say to each other once the connection exists. That's HTTP's job. It's the layer that turns "I have an IP and an open connection" into "give me this page" and "here's your JSON." And unlike DNS, which is one clean question-and-answer flow, HTTP has quietly gone through several structural rewrites — 1.0, 1.1, 2 — each one solving a specific bottleneck the previous version created. Understanding HTTP means understanding why those rewrites happened, not just what the current version looks like.
2. Source Material
Playlist: Chai aur Code — Computer Networking Series
Video: How HTTP Works
Video: HTTP/1.1 vs HTTP/2
3. What I Learned
HTTP is deliberately a thin layer. It doesn't transport anything itself, doesn't encrypt anything itself, and doesn't guarantee delivery itself — it hands all three of those jobs to TCP (and to TLS, for encryption) and focuses purely on the shape of a request and a response. That separation is why it can serve JSON, HTML, video, or a PDF with zero changes to the protocol itself — media independence isn't a feature bolted on, it's a consequence of HTTP not caring what's inside the body.
What actually changed release to release is the connection model, not the message format. HTTP/1.0 opened and closed a fresh TCP connection — full 3-way handshake included — for every single request. That's fine for one page load, brutal for a page with forty assets. HTTP/1.1 made persistent connections the default, so one TCP connection could serve many requests back to back. But "back to back" still meant one at a time — you couldn't fire request #2 until response #1 came home.
Pipelining tried to fix that by letting the client queue multiple requests without waiting for each response, on the condition that the server had to answer in the exact same order they were asked. That condition is where it falls apart: response sizes vary, so a slow response at the front blocks fast ones behind it from ever being delivered early. That's head-of-line blocking, and it's the reason pipelining is disabled by default almost everywhere — it protects an ordering guarantee that costs more than it saves.
HTTP/2's fix is more fundamental than "allow more requests." It gives every request/response pair its own identifier — a stream ID — so order stops being the thing that ties a response back to its request. That one change is what makes true multiplexing possible: many requests and responses interleaved on a single TCP connection, arriving in whatever order the server finishes them, each one still landing correctly because the stream ID says who it belongs to.
4. Key Concepts
Application layer — HTTP sits at the top of the stack; it defines message semantics, not transport.
Statelessness — each HTTP request is independent by default; the protocol itself has no memory of previous requests. This is exactly why Cookie exists as a header at all — it's not part of HTTP's core design, it's a workaround bolted on specifically because the protocol refuses to remember anything on its own.
Persistent connection (keep-alive) — a single TCP connection reused across multiple request/response cycles, avoiding a fresh handshake per request.
Head-of-line (HOL) blocking — when one slow response blocks other, already-ready responses from being delivered because ordering must be preserved.
Pipelining — sending multiple HTTP/1.1 requests without waiting for each response, constrained by strict in-order delivery.
Stream ID — HTTP/2's per-request/response identifier, replacing "order" as the mechanism that matches a response to its request.
Multiplexing — multiple requests and responses interleaved concurrently over one TCP connection.
ETag — a hash of a response body, used to let a client ask "has this changed?" without re-downloading unchanged data.
Server Push — HTTP/2 feature where the server proactively sends resources it expects the client will need next.
5. How It Works
The Handshake-Per-Request Problem (HTTP/1.0)
Client Server
|--- TCP 3-way handshake ------->|
|--- HTTP request --------------->|
|<-- HTTP response ---------------|
|--- TCP connection closes ------|
(repeat the entire handshake for the NEXT request)
Every request pays full TCP setup and teardown cost. Connection: keep-alive existed even here as an opt-in escape hatch, but it wasn't the default.
Persistent Connections, One Request at a Time (HTTP/1.1)
Client Server
|--- TCP handshake (once) ------>|
|--- Request 1 ------------------>|
|<-- Response 1 ------------------|
|--- Request 2 ------------------>|
|<-- Response 2 ------------------|
...
|--- Connection closes (client-initiated) -->|
One handshake, many requests — but strictly sequential. Request 2 can't go out until Response 1 has come back.
Pipelining and Where It Breaks
Pipelining isn't a separate connection mode — it's still the same single persistent TCP connection from the diagram above. The only thing that changes is the client no longer waits for each response before sending the next request:
Same persistent connection as before:
Client sends: Req A ---> Req B ---> Req C (no waiting between sends)
Server must reply: Resp A ---> Resp B ---> Resp C (same order, no exceptions)
If A is a 5 MB file and B is a 2 KB file, B still waits behind A on the wire — even though B finished processing first. That's head-of-line blocking, and it's why pipelining ships disabled by default almost everywhere.
Request/Response Anatomy
Request headers worth knowing:
| Header | What it's for |
|---|---|
Host |
Which domain/subdomain this request is for — the same server IP can host many domains, so this is how it disambiguates |
Referer |
The page the request originated from |
Accept |
What response formats the client will actually accept |
Accept-Encoding / Accept-Language |
Compression and language preferences |
User-Agent |
Identifies the client |
Cookie |
Session/state data |
Response headers worth knowing:
| Header | What it's for |
|---|---|
Content-Type / Content-Length |
What the body is and how big it is |
Server |
Server software/OS info |
ETag |
Hash of the response body |
Connection |
Connection-handling directives |
ETags: Caching That Saves Bandwidth, Not Compute
1. Client: GET /users
2. Server: builds JSON → hashes it → ETag: "abc123"
responds with JSON + ETag
3. Client (later): GET /users
If-None-Match: "abc123"
4. Server: builds JSON again → hashes it again → "abc123"
same hash as before →
responds: 304 Not Modified (no body)
The server still does the full DB round trip and JSON build every time — ETags don't save it any processing. What they save is the client re-downloading a body that hasn't changed.
HTTP/2: Multiplexing via Stream IDs
Client: GET index.html (stream 1)
Server: RES index.html (stream 1)
Server: PUSH main.css (stream 2) ← server-initiated → even ID
Client: GET about.html (stream 3) ← next client request → odd ID
Server: RES about.html (stream 3)
Server: PUSH script.js (stream 4) ← next server push → even ID
Client-initiated streams get odd IDs, server-pushed streams get even IDs — so the two numbering sequences never collide, and responses can arrive in any order without ambiguity about which request they answer.
Worth flagging: Server Push is the one HTTP/2 feature that hasn't held up well in practice. Browsers ended up bad at telling which pushed resources they actually needed versus already had cached, so pushes often wasted bandwidth instead of saving time. Chrome dropped support for it; most of the industry has shifted to 103 Early Hints instead, which tells the client what to start fetching without the server guessing and pushing blind. Good to know the mechanism, but it's not something you'll see relied on in a modern stack.
6. Things That Confused Me
I initially read "HTTP doesn't guarantee delivery" as a flaw, before realizing it's not HTTP's job to guarantee that in the first place — that's TCP's contract, sitting one layer down. HTTP inherits TCP's reliability for free and never re-implements it, which is exactly why the protocol itself can stay this simple.
Pipelining was the other trip-up. It sounds like a strict improvement over sequential 1.1 — send more, wait less — until you see the ordering constraint attached to it. It's not "send whenever," it's "send whenever, but the response order is non-negotiable," and that one constraint is enough to make it worse than useless for real traffic with mixed response sizes. HTTP/2 didn't just add more concurrency on top of the same idea — it removed the constraint entirely by decoupling "which response is this" from "what order did responses arrive in."
I also assumed ETags were a performance optimization for the server. They're not — the server does identical work (query, build, hash) whether or not the ETag matches. The entire saving is on the wire, for the client, in the form of a body it doesn't have to re-download.
7. My Explanation in Simple Words
Think of HTTP/1.0 as calling someone, asking one question, hanging up, then dialing again from scratch for the next question. HTTP/1.1 is realizing you can just stay on the call and ask your next question without redialing — but you still have to wait for them to answer question 1 before you ask question 2.
Pipelining is trying to rapid-fire questions 1, 2, and 3 without waiting — but insisting the answers come back in exactly that order. If question 1 needs them to go check a filing cabinet and question 2 is something they know off the top of their head, you're still stuck waiting for the filing cabinet before you get the easy answer, even though it was ready first.
HTTP/2 fixes this by tagging every question and every answer with a number. Now they can answer question 2 the instant they know it, out of order, and you'll still know exactly which question it belongs to because it's labeled. Nobody has to wait their turn anymore.
8. Key Takeaways
HTTP is intentionally thin: it defines request/response shape and delegates transport reliability to TCP and encryption to TLS — media independence and language independence both fall out of that separation, not the other way around.
Every version bump from 1.0 to 2 is a fix for a specific bottleneck: 1.0's per-request handshake cost, 1.1's strict sequential requests, and pipelining's ordering constraint that produces head-of-line blocking.
HTTP/2's real innovation isn't "more concurrency" — it's replacing order with a stream ID as the thing that matches a response to its request, which is what makes true multiplexing (and Server Push, with its odd/even ID split) possible.
ETags trade zero server compute for real bandwidth savings — the server still does the full work every time; the client is the one who skips the download when nothing changed.
Head-of-line blocking is the throughline across this entire post: it's the failure mode HTTP/1.1 pipelining runs into, and the specific problem HTTP/2's design set out to eliminate.
9. Next Topic
Next up: HTTP/2 — a closer look at multiplexing, stream prioritization, and Server Push, going deeper than the surface-level pass this post took.



