HTTP and the Web
Every time a browser loads a page, dozens of independent request-response exchanges happen in seconds. A single page load might fetch an HTML document, three stylesheets, a dozen images, and two scripts. Each of these requires its own conversation with a server. Understanding how these conversations are structured is essential. This means understanding what is asked, what is answered, and what state persists between them. This is the foundation for understanding the modern web. HTTP (HyperText Transfer Protocol) is the application-layer protocol that makes this possible. It defines a simple, text-based request-response model. This model sits on top of reliable transport, typically TCP. It increasingly runs over encrypted channels via TLS. Its genius is not cleverness but restraint. It relies on a small vocabulary of methods, a numeric status code system, and extensible headers. These headers let the protocol evolve without breaking old clients or servers. This article builds HTTP from first principles. It covers the request-response cycle, the methods and status codes that give it meaning, and the headers that carry metadata. It also covers the mechanisms — persistent connections, cookies, caching — that turn a stateless protocol into the backbone of stateful, performant web applications. We assume familiarity with sockets and DNS resolution. This is because HTTP begins only after a name is resolved and a transport connection is either opened or reused. By the end, you should be able to trace an HTTP exchange byte-by-byte. You should be able to explain why HTTP is stateless yet websites remember you. You should be able to reason about when a cached response can legally be reused. You should also see the specific bottlenecks that motivated HTTP/2 and HTTP/3.
- HTTP is a stateless, text-based request-response protocol: a client sends a request, a server sends a response.
- Requests carry a method (GET, POST, PUT, DELETE, ...), a target, headers, and an optional body.
- Responses carry a three-digit status code (2xx success, 3xx redirect, 4xx client error, 5xx server error), headers, and a body.
- Persistent connections (`Connection: keep-alive`) let multiple request-response pairs reuse one TCP connection, avoiding repeated handshake overhead.
- Cookies restore state across stateless requests; the server sets them via `Set-Cookie`, and the client echoes them via `Cookie`.
- Caching (via `Cache-Control`, `ETag`, `Last-Modified`) avoids re-fetching unchanged resources, trading a little staleness risk for large latency and bandwidth savings.
HTTP is a deliberately simple, stateless request-response protocol whose power comes from layering state (cookies), performance (persistence, caching), and security (TLS) on top rather than baking them into the core.
The idea
HTTP defines a request-response model: a client opens a connection to a server, sends a request message, and the server sends back a response message. Each request names a method (the action requested), a target (usually a path and query string), a version, a set of header fields (metadata as key-value pairs), and an optional message body. Each response mirrors this structure with a status code in place of a method — a three-digit number plus a short reason phrase indicating how the request was handled. Critically, HTTP is stateless: by design, the server retains no memory of previous requests from the same client. Each request must be self-contained, carrying whatever context it needs. This simplicity is what let HTTP scale to billions of clients — servers need not track per-client session objects at the protocol level. But real applications do need state (shopping carts, login sessions), so the ecosystem layers state on top using cookies: small pieces of data the server asks the client to store and resend on every future request to that domain. A second core idea is resource identification. Every HTTP request targets a URI (Uniform Resource Identifier), typically a URL like `https://example.com/products/42?ref=email`. The scheme (`https`) tells the client to use TLS; the authority (`example.com`) is resolved via DNS to an IP address; the path and query identify a specific resource on that server. HTTP methods express what to do with that resource — retrieve it (`GET`), create or update it (`POST`, `PUT`), remove it (`DELETE`) — following a design philosophy sometimes called REST, where URIs name resources and methods name verbs applied to them.
How it works
Step 1 — Connection establishment. The client resolves the hostname via DNS, then opens a TCP connection (usually port 443 for HTTPS, 80 for plain HTTP), performing a TCP three-way handshake and, for HTTPS, a TLS handshake on top.
Step 2 — Request construction. The client builds a request message with a start line, headers, a blank line, and an optional body: ``` GET /products/42?ref=email HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0 Accept: text/html Cookie: session=abc123 Connection: keep-alive ```
Step 3 — Transmission. The request is sent over the (possibly encrypted) TCP stream.
Step 4 — Server processing. The server parses the request line and headers, locates or computes the requested resource, and decides on a status code.
Step 5 — Response construction. The server builds a response: ``` HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 4210 Cache-Control: max-age=3600 Set-Cookie: session=abc123; HttpOnly; Secure Connection: keep-alive ... ```
Step 6 — Connection reuse or close. If `Connection: keep-alive` (the default in HTTP/1.1) is negotiated, the same TCP connection remains open for subsequent requests to the same server, avoiding a new handshake per resource. Otherwise the connection closes after the response.
Step 7 — Client rendering / follow-up requests. The client parses the response; if it's HTML, it discovers embedded resources (images, CSS, scripts) and issues further requests, often over the same persistent connection or over additional parallel connections. Below is a simplified pseudocode model of a client's request lifecycle, including cache and cookie handling: ```pseudocode function fetch(url, method="GET", body=None): scheme, host, path, query = parse_url(url) // 1. Check local cache first cached = cache_lookup(host, path, query) if cached is not None and not cached.is_stale(): return cached.response // cache hit, no network trip // 2. Reuse or open a connection conn = connection_pool.get(host, scheme) if conn is None or conn.closed: conn = open_tcp_connection(host, port_for(scheme)) if scheme == "https": conn = tls_handshake(conn, host) connection_pool.put(host, conn) // 3. Build request headers = default_headers(host) cookie_value = cookie_jar.get(host, path) if cookie_value is not None: headers["Cookie"] = cookie_value if cached is not None: // conditional revalidation if cached.etag is not None: headers["If-None-Match"] = cached.etag if cached.last_modified is not None: headers["If-Modified-Since"] = cached.last_modified request = build_request(method, path, query, headers, body) // 4. Send and receive send(conn, request) response = receive_response(conn) // 5. Handle 304 Not Modified if response.status == 304: cached.refresh_validity(response.headers) return cached.response // 6. Update cookie jar if "Set-Cookie" in response.headers: cookie_jar.store(host, response.headers["Set-Cookie"]) // 7. Update cache if is_cacheable(method, response): cache_store(host, path, query, response) // 8. Close connection if server requested it if response.headers.get("Connection") == "close": conn.close() connection_pool.remove(host, conn) return response ```
Complexity & correctness
Latency cost model. Let RTT be the round-trip time between client and server. Establishing a fresh TCP connection costs 1 RTT (handshake), and TLS 1.3 adds roughly 1 RTT (or 0 with session resumption / 0-RTT). Each HTTP request-response pair costs at least 1 RTT for the data exchange itself. Without persistent connections, fetching n resources from the same server costs approximately:
because each resource requires its own connection setup. With persistent connections, the handshake cost is paid once:
(assuming requests are pipelined or sent back-to-back rather than each waiting fully for the previous response — a limitation that HTTP/1.1 partially addresses and that HTTP/2 and HTTP/3 solve more thoroughly with multiplexing). This is why persistent connections were one of the most impactful additions from HTTP/1.0 to HTTP/1.1: the savings scale linearly with the number of subresources on a page, and a typical page has dozens.
Caching correctness. A cache is correct if it never serves a response that the origin server would disavow as stale, subject to the freshness rules the server itself specified. The key invariant: > A cached response may be served without contacting the origin only if its age is within the freshness window the server declared (via `Cache-Control: max-age` or an `Expires` date), or the client has revalidated it (via a conditional request) and received confirmation (`304 Not Modified`) that it is unchanged. This invariant is maintained by construction in the pseudocode above: `is_stale()` checks the freshness window, and revalidation always attaches `If-None-Match` / `If-Modified-Since` before trusting a cached body past its freshness window. The server, not the cache, is the source of truth for what "unchanged" means, so correctness reduces to the server computing `ETag`/`Last-Modified` faithfully — a comparison of resource identity, which is a simple equality check server-side, not a client-side heuristic.
Statelessness and cookies. Because HTTP itself carries no session state, correctness of "logged-in" behavior depends entirely on the cookie mechanism functioning as an out-of-band key-value store keyed by domain and path, per the same-origin rules. The protocol's statelessness is not a limitation to work around silently — it is an explicit design tradeoff: simplicity and horizontal scalability of servers (any server behind a load balancer can handle any request, since no server-local session state is required) in exchange for applications needing an explicit state layer (cookies plus server-side session stores or JWTs).
Complexity of request routing. Server-side URL routing (mapping a path to a handler) is typically implemented as a trie or hash-map lookup over registered route patterns, giving average-case dispatch time where is the path length — independent of the number of registered routes if a trie is used, versus for naive linear pattern matching over routes.
Honest tradeoffs.
- Persistent connections reduce handshake overhead but consume server memory/file descriptors per open connection — a scalability tension addressed by connection timeouts and, later, multiplexed protocols.
- Aggressive caching improves latency but risks serving stale content; `Cache-Control: no-cache` forces revalidation at the cost of an extra round trip even when content hasn't changed.
- Cookies restore statefulness but introduce privacy and security surface area (session hijacking, cross-site tracking), which is why `HttpOnly`, `Secure`, and `SameSite` attributes exist as mitigations.
Worked example
Trace a browser loading `http://www.example.org/index.html`, which references one image, with a plain (non-persistent) HTTP/1.0-style interaction versus an HTTP/1.1 persistent-connection interaction. Assume RTT = 50 ms, and DNS is already resolved.
| Step | HTTP/1.0 (new connection per object) | HTTP/1.1 (persistent connection) |
|---|---|---|
| 1 | TCP handshake to fetch `index.html`: 50 ms | TCP handshake (once): 50 ms |
| 2 | GET `/index.html` request/response: 50 ms | GET `/index.html` request/response: 50 ms |
| 3 | Parse HTML, discover ` ` | Parse HTML, discover ` ` |
| 4 | New TCP handshake for image: 50 ms | (connection already open — no handshake) |
| 5 | GET `/logo.png` request/response: 50 ms | GET `/logo.png` request/response over same connection: 50 ms |
| Total | 200 ms | 150 ms |
With just one extra object the saving is 50 ms (25%); with 20 subresources (typical of a media-rich page), HTTP/1.0-style behavior would cost roughly `50 + 20×(50+50) = 2050` ms versus `50 + 20×50 = 1050` ms for persistent connections — nearly a 2× improvement, purely from eliminating repeated handshakes, before any consideration of parallelism. Now trace a caching scenario for the same image on a repeat visit, assuming the first response included: ``` Cache-Control: max-age=86400 ETag: "d41d8cd98f00b204" ```
| Visit | Time since first fetch | Cache decision | Network trip? |
|---|---|---|---|
| First load | 0s | No cached entry | Full GET, 200 OK, body + headers stored |
| Reload after 1 hour | 3600s < 86400s | Fresh — serve from cache | None — 0 RTT |
| Reload after 2 days | 172800s > 86400s | Stale — revalidate | `If-None-Match: "d41d8cd98f00b204"` sent |
| — server unchanged | Server compares ETag, matches | `304 Not Modified`, no body — 1 RTT, saves bandwidth but not the trip | |
| Reload after edit | any | Server ETag differs | Full `200 OK` with new body and new ETag |
This demonstrates the core caching tradeoff precisely: within the freshness window, caching eliminates the network round trip entirely; outside it, revalidation still saves bandwidth (no body transferred) even though it cannot save the round trip itself.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Treating HTTP as stateful | Assuming the server "remembers" a prior request without cookies/session tokens | Broken login flows, lost shopping carts across requests |
| Forgetting `Content-Length` or chunked encoding | Server doesn't know how much body to send/receive | Client hangs waiting for more data, or truncates response |
| Confusing `301` and `302`/`307` | Not distinguishing permanent vs. temporary redirects | Search engines cache wrong canonical URL; POST bodies dropped on 302 by some clients |
| Caching `POST` responses | Assuming all responses are cacheable by default | Stale or duplicated side-effecting operations served from cache |
| Missing `Vary` header | Caching a response that differs by `Accept-Language` or `User-Agent` without declaring it | Wrong-language or wrong-format page served to a different user from cache |
| Reusing a persistent connection after `Connection: close` | Ignoring the server's explicit signal to close | Client sends into a half-closed socket, gets connection reset |
| Storing cookies without `Secure`/`HttpOnly` | Overlooking that cookies are plain text over HTTP and JS-readable by default | Session hijacking via network sniffing or XSS |
Pioneers & history
HTTP emerged from a very specific, concrete need: physicists at CERN wanted to share documents across incompatible computer systems without manually converting formats. Tim Berners-Lee's proposal combined three simple ideas — URLs to name resources, HTML to structure documents, and HTTP to transfer them — into what became the World Wide Web.
Origins · 1 minTim Berners-Lee and how this was discovered
Working as a software engineer at CERN, Berners-Lee needed a way for physicists worldwide to link and retrieve documents stored on different, incompatible systems. He designed HTTP alongside HTML and the URL scheme, implementing the first web browser/editor and the first web server on a NeXT computer in 1990. His original HTTP/0.9 was deliberately minimal: a single-line request (just `GET /path`) and a raw HTML response with no headers at all — the entire protocol's initial surface fit in a paragraph, a stark contrast to the header-rich, versioned protocol it became.
Where this matters
Every browser, mobile app backend, and REST API depends on the request-response semantics described here. Content Delivery Networks like Cloudflare and Akamai exist largely to exploit HTTP caching semantics at scale, placing edge servers close to users so that `Cache-Control` and `ETag` validation happen with minimal latency — a mechanism explored further in Content Delivery Networks. Web application frameworks (Django, Express, Spring) are fundamentally HTTP request routers layered with session/cookie management. Browsers' developer tools (the Network tab) expose exactly the request/response headers and status codes described here, and debugging a "why is my page slow" or "why is stale data showing" problem almost always comes down to reasoning about persistent connections, caching headers, or cookie scope. The security properties layered on top — via TLS and Secure Channels — protect the confidentiality and integrity of every request and response, including the cookies that carry session identity. And the performance limitations of HTTP/1.1's connection model (head-of-line blocking, one request "in flight" per stream) directly motivated the multiplexed, binary framing of HTTP/2 and HTTP/3. Even email retrieval protocols like those covered in Email Protocols: SMTP, IMAP, POP3 share the same request-response philosophy, though tuned for message delivery rather than document retrieval.
Ask the tutor: How does HTTP/2's multiplexing change the request-response model described here, and why does HTTP/3 need a new transport protocol to fully solve head-of-line blocking?
Why does HTTP use plain text instead of a binary format?
HTTP/0.9 through HTTP/1.1 use human-readable text for requests and headers because it made the protocol easy to debug, implement, and extend in the protocol's early days — you could literally type a request by hand using `telnet`. This readability came at a real cost: parsing text is slower and more error-prone than parsing fixed binary fields, and text headers repeat verbose field names on every request. As the web scaled to billions of requests per second, this overhead became significant, which is exactly why HTTP/2 and HTTP/3 moved to a binary framing layer with header compression (HPACK/QPACK), while preserving the same conceptual request-response semantics and status codes underneath.
What is the difference between a 301 and a 302 redirect?
Both tell the client "go elsewhere," but they differ in permanence and caching implications. A `301 Moved Permanently` tells the client (and search engines) that the resource has permanently relocated; clients should update bookmarks and search engines should re-index the new URL, and browsers may cache this redirect aggressively. A `302 Found` (or the more precise `307 Temporary Redirect`) signals a temporary relocation — the original URL remains canonical and should be retried in the future. Using 301 for a temporary change can cause search engines to drop the old URL permanently, while using 302 for a permanent change forces every future visitor through an unnecessary extra round trip.
How do cookies actually work at the protocol level?
A cookie is just a header exchange. When a server wants to establish state, it sends `Set-Cookie: name=value; attributes` in a response. The browser stores this value, scoped to the domain (and optionally path) specified. On every subsequent request to a matching domain/path, the browser automatically attaches `Cookie: name=value` in the request headers — no application code needs to manually resend it. Attributes like `Expires`/`Max-Age` control lifetime, `HttpOnly` prevents JavaScript access (mitigating XSS-based theft), `Secure` restricts transmission to HTTPS only, and `SameSite` restricts cross-site sending (mitigating CSRF). The protocol itself has no concept of a "session" — sessions are an application-layer convention built entirely on this cookie mechanism.
Why can a cached response become incorrect, and how does HTTP prevent that?
A cache is a bet that a resource hasn't changed since it was fetched. If the server updates the resource within the cache's freshness window, clients relying purely on `max-age` will see stale data until that window expires — this is an inherent tradeoff, not a bug. HTTP mitigates this with conditional requests: `If-None-Match` (comparing an `ETag`, a server-computed content fingerprint) or `If-Modified-Since` (comparing a timestamp) let a client ask "has this changed?" without re-downloading the body if it hasn't. Servers can also set `Cache-Control: no-cache` to force revalidation on every use, or `no-store` to forbid caching altogether for highly sensitive or highly volatile content.
Why does HTTP/1.1 sometimes still feel slow despite persistent connections?
Persistent connections solve repeated handshake overhead but not head-of-line blocking: within a single HTTP/1.1 connection, responses must generally arrive in the order requests were sent, so one slow resource blocks everything queued behind it on that connection. Browsers historically worked around this by opening multiple parallel TCP connections per host (often 6), trading one bottleneck for the cost of more handshakes and increased server load. This structural limitation — one connection, strictly ordered responses — is precisely the problem HTTP/2 and HTTP/3 solve by multiplexing many independent streams over one connection (or, in HTTP/3's case, over QUIC to also avoid blocking at the transport layer).
What role does DNS play before HTTP even begins?
Before any HTTP request is sent, the client must translate the human-readable hostname (like `www.example.org`) into an IP address, which is the job of DNS: The Internet Naming System, entirely separate from HTTP itself. This resolution typically happens once and is cached by the operating system or browser, so it does not repeat for every request to the same host within its TTL. Only after DNS resolution succeeds can the client open the TCP connection HTTP relies on, which is why DNS failures manifest to users as "site can't be reached" errors rather than any HTTP-level status code — the failure occurs a full layer beneath HTTP.
Is HTTP itself secure, and where does TLS fit in?
Plain HTTP transmits everything — including headers, cookies, and bodies — as cleartext over the network, visible to anyone who can observe the connection. HTTPS is simply HTTP layered on top of TLS, which negotiates encryption and authenticates the server (via certificates) before any HTTP bytes are exchanged. The request-response semantics, methods, status codes, and headers described here are entirely unchanged by TLS — it operates transparently beneath HTTP, encrypting the same messages rather than altering their structure. This is why cookie attributes like `Secure` matter: they instruct the browser never to send that cookie over a non-TLS connection, closing an otherwise easy interception vector.
`