HTTP/1.0 and HTTP/1.1: The Foundation
From one-request-per-connection to persistent connections, chunked transfer, and the Host header that saved the internet.
- DATE:
- APR.28.2026
- READ:
- 15 MIN
HTTP/0.9 — the one-line protocol
In 1991, Tim Berners-Lee at CERN needed a way to fetch hypertext documents from a server. The protocol he created was barely a protocol at all. The client opens a TCP connection, sends a single line:
GET /index.htmlThe server responds with raw HTML and immediately closes the connection. No headers. No status codes. No content types. No version negotiation. If the document doesn’t exist, the server might return an error message in HTML — or just close the socket. The client has no machine-readable way to tell the difference.
This was fine when the entire World Wide Web was a few hundred pages on a handful of machines. It stopped being fine almost immediately.
HTTP/1.0 — RFC 1945 (May 1996)
By 1996, the web had exploded beyond anything Berners-Lee anticipated. Browsers loaded images, forms submitted data, and servers needed to communicate metadata about responses. HTTP/1.0, documented in RFC 1945 by Tim Berners-Lee, Roy Fielding, and Henrik Frystyk Nielsen, was an informational RFC — it didn’t define a new protocol so much as it wrote down what browsers and servers were already doing.
The key addition: headers and status codes. A request/response now looked like this:
GET /index.html HTTP/1.0
User-Agent: Mozilla/3.0 (Win95; I)
Accept: text/html
HTTP/1.0 200 OK
Date: Tue, 14 May 1996 12:00:00 GMT
Content-Type: text/html
Content-Length: 1234
<html>
<body>Hello, world.</body>
</html>Headers gave both sides a way to negotiate content types, declare encodings, identify themselves, and communicate caching information. Status codes gave the client a machine-readable signal: 2xx for success, 3xx for redirection, 4xx for client errors, 5xx for server errors.
But HTTP/1.0 had a fundamental limitation: one request per TCP connection. Every image, every stylesheet, every script required a full TCP handshake (SYN, SYN-ACK, ACK), a single request/response exchange, and then a connection teardown (FIN). On a page with 30 resources, that’s 30 TCP handshakes. Over a high-latency link, the cost was brutal.
Caching was primitive. The only tools were Expires (an absolute timestamp that breaks if clocks drift) and If-Modified-Since / Last-Modified (which can’t handle sub-second changes and is unreliable across distributed backends). There was no Host header requirement, so a server listening on a single IP could only serve one website. There was no chunked transfer encoding, so the server had to know the full Content-Length before sending the first byte.
Netscape’s engineers knew the one-connection-per-request model was killing performance. They introduced a non-standard Connection: keep-alive header that both client and server could use to signal “don’t close this connection after the response.” It worked, mostly, but it wasn’t part of the spec. Proxies that didn’t understand it would hang. It was a hack, and everyone knew it.
HTTP/1.1 — RFC 2068 to RFC 9112
HTTP/1.1 arrived in January 1997 as RFC 2068 — only eight months after 1.0 was published. It has been revised multiple times since:
+------+---------------+-------------------+--------------------+ | Year | RFC | Status | Notes | +------+---------------+-------------------+--------------------+ | 1997 | RFC 2068 | Proposed Standard | First HTTP/1.1 | | | | | spec | +------+---------------+-------------------+--------------------+ | 1999 | RFC 2616 | Draft Standard | Consolidated | | | | | HTTP/1.1, lasted | | | | | 15 years | +------+---------------+-------------------+--------------------+ | 2014 | RFC 7230-7235 | Proposed Standard | Split into six | | | | | focused documents | +------+---------------+-------------------+--------------------+ | 2022 | RFC 9110-9112 | Internet Standard | Current definitive | | | | | spec, merged | | | | | semantics across | | | | | versions | +------+---------------+-------------------+--------------------+
The 2014 revision (RFC 7230-7235) split the monolithic RFC 2616 into six focused documents. The 2022 revision (RFC 9110-9112) collapsed HTTP semantics into a single document shared across HTTP/1.1, HTTP/2, and HTTP/3, reflecting the reality that the semantics are version-independent.
HTTP/1.1 is the version that made the modern web possible. Every major improvement addressed a specific, painful limitation of 1.0.
Persistent connections
In HTTP/1.1, connections are persistent by default. The client doesn’t need to ask for keep-alive; it gets it unless someone sends Connection: close. A single TCP connection can carry dozens or hundreds of sequential request/response pairs.
This eliminated the TCP handshake overhead for subsequent requests. On a page loading 40 resources from the same origin, the savings were enormous — especially before TLS 1.3 reduced the TLS handshake from two round trips to one (or zero, with 0-RTT).
GET /style.css HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 4096
/* ... CSS ... */
GET /app.js HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Content-Type: application/javascript
Content-Length: 28672
// ... JS ...Same TCP connection, no re-handshake. The improvement in page load times was immediate and dramatic.
The Host header
HTTP/1.1 made the Host header mandatory on every request. This is easy to overlook and hard to overstate.
Before Host, a server bound to 93.184.216.34 had no way to know whether an incoming request was for example.com, example.org, or shop.example.com. The HTTP request line contained only the path. The server saw GET /index.html and had to guess which site you meant.
This meant one IP address per website. IPv4 has roughly 4.3 billion addresses. There are over 1 billion websites today. The math doesn’t work.
The Host header enabled name-based virtual hosting: a single server, on a single IP, inspecting the Host header to route requests to the correct site. Apache’s <VirtualHost> directive, nginx’s server_name, and every shared hosting provider on earth depend on this header. Without it, the web could not have scaled.
Chunked transfer encoding
HTTP/1.0 required Content-Length in the response headers. If the server didn’t know the total size in advance — because it was streaming data, generating content dynamically, or proxying from a slow upstream — it had to buffer the entire response before sending headers.
HTTP/1.1 introduced Transfer-Encoding: chunked. The server sends data in chunks, each prefixed with its size in hexadecimal, terminated by a zero-length chunk:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
1a
This is the first chunk.
1c
And this is the second one.
0
This enabled streaming responses, Server-Sent Events (SSE), and dynamic content generation without buffering. It also made proxies more efficient — they could forward chunks as they arrived instead of buffering entire responses.
Pipelining (and why it died)
HTTP/1.1 introduced pipelining: the client could send multiple requests on the same connection without waiting for each response. In theory, this would eliminate round-trip latency for sequential requests.
Client Server
|--- GET /a.css ---> |
|--- GET /b.js ---> |
|--- GET /c.png ---> |
| <--- 200 /a.css ---|
| <--- 200 /b.js ---|
| <--- 200 /c.png ---|In practice, pipelining had a fatal flaw: head-of-line (HOL) blocking. Responses must be returned in the same order as requests. If /a.css takes 500ms to generate and /b.js is ready in 2ms, the server must hold /b.js until /a.css finishes. One slow response blocks everything behind it.
It got worse. Proxies that didn’t implement pipelining correctly would corrupt response streams. Error recovery was ambiguous — if the connection drops mid-pipeline, which requests were processed? Non-idempotent requests (POST) couldn’t be safely pipelined at all.
No major browser ever shipped pipelining enabled by default. Firefox had it behind a flag (network.http.pipelining) for years and eventually removed it. It took 18 years for HTTP/2 to solve what pipelining tried to address, using a fundamentally different approach: binary framing with multiplexed streams.
Cache-Control and ETags
HTTP/1.0’s caching was limited to Expires and Last-Modified. HTTP/1.1 introduced Cache-Control, a header with fine-grained directives:
+-----------------+------------------+----------------------------------+ | Directive | Scope | Meaning | +-----------------+------------------+----------------------------------+ | max-age=N | Request/Response | Fresh for N seconds from | | | | response time | +-----------------+------------------+----------------------------------+ | no-cache | Request/Response | Must revalidate before use (does | | | | NOT mean "don't cache") | +-----------------+------------------+----------------------------------+ | no-store | Request/Response | Do not cache at all, anywhere | +-----------------+------------------+----------------------------------+ | private | Response | Only browser may cache, not | | | | shared caches (CDNs, proxies) | +-----------------+------------------+----------------------------------+ | public | Response | Any cache may store, even if | | | | response is normally | | | | non-cacheable | +-----------------+------------------+----------------------------------+ | must-revalidate | Response | Once stale, must revalidate — no | | | | serving stale on error | +-----------------+------------------+----------------------------------+ | s-maxage=N | Response | Like max-age but only for shared | | | | caches (overrides max-age) | +-----------------+------------------+----------------------------------+
HTTP/1.1 also introduced ETag (entity tag) — an opaque validator, typically a hash or version string, that the server attaches to a response. On subsequent requests, the client sends If-None-Match: "etag-value". If the resource hasn’t changed, the server responds with 304 Not Modified and no body.
ETag / If-None-Match is strictly better than Last-Modified / If-Modified-Since for several reasons: it handles sub-second changes, it works across distributed backends that might have different filesystem timestamps, and it can reflect content equivalence rather than timestamp equality. In practice, most systems use both for backward compatibility.
Range requests
HTTP/1.1 added Range requests via the Range header and 206 Partial Content response. A client can request a specific byte range:
GET /video.mp4 HTTP/1.1
Host: cdn.example.com
Range: bytes=1048576-2097151
HTTP/1.1 206 Partial Content
Content-Range: bytes 1048576-2097151/104857600
Content-Length: 1048576
(1 MB of video data)This enabled three things that HTTP/1.0 couldn’t do: resumable downloads (if a 500MB download fails at 400MB, restart from byte 400000000), video seeking (jump to minute 45 without downloading the first 44 minutes), and parallel downloads (split a large file across multiple Range requests). Every video player, download manager, and CDN depends on this.
New methods
HTTP/1.0 defined three methods: GET, HEAD, POST. HTTP/1.1 expanded the vocabulary:
+---------+-------------+-------------+------------------------------+ | Method | RFC | Year | Purpose | +---------+-------------+-------------+------------------------------+ | GET | 1945 / 9110 | 1996 / 2022 | Retrieve a resource | +---------+-------------+-------------+------------------------------+ | HEAD | 1945 / 9110 | 1996 / 2022 | GET without the body | +---------+-------------+-------------+------------------------------+ | POST | 1945 / 9110 | 1996 / 2022 | Submit data, create | | | | | resources | +---------+-------------+-------------+------------------------------+ | PUT | 2068 / 9110 | 1997 / 2022 | Replace a resource entirely | +---------+-------------+-------------+------------------------------+ | DELETE | 2068 / 9110 | 1997 / 2022 | Remove a resource | +---------+-------------+-------------+------------------------------+ | OPTIONS | 2068 / 9110 | 1997 / 2022 | Discover allowed methods | | | | | (crucial for CORS preflight) | +---------+-------------+-------------+------------------------------+ | CONNECT | 2068 / 9110 | 1997 / 2022 | Establish a tunnel (HTTPS | | | | | through proxies) | +---------+-------------+-------------+------------------------------+ | TRACE | 2068 / 9110 | 1997 / 2022 | Loop-back diagnostic (almost | | | | | universally disabled) | +---------+-------------+-------------+------------------------------+ | PATCH | 5789 | 2010 | Partial modification of a | | | | | resource | +---------+-------------+-------------+------------------------------+
OPTIONS deserves special attention. It was originally designed for capability discovery, but its critical modern use is as the CORS preflight mechanism. When a browser makes a cross-origin request with non-simple headers or methods, it first sends an OPTIONS request. The server’s response determines whether the actual request is allowed. Every cross-origin API call in every modern web application depends on this method.
PATCH arrived late — RFC 5789 wasn’t published until March 2010 — but it filled a real gap. PUT replaces an entire resource; PATCH applies a partial modification. The distinction matters for bandwidth, concurrency, and API design.
Head-of-line blocking — the fundamental problem
HTTP/1.1’s single biggest limitation is head-of-line (HOL) blocking, and it operates at two levels.
Application-level HOL blocking. On a persistent connection, responses must be delivered in request order. If request 1 takes 2 seconds and requests 2-10 are ready in 5ms each, all nine fast responses wait behind the slow one. Pipelining was supposed to help by sending all ten requests at once, but the ordering constraint means the responses still queue up.
Transport-level HOL blocking. TCP guarantees in-order byte delivery. If packet 3 of 10 is lost, the kernel buffers packets 4-10 and waits for packet 3’s retransmission. The application sees a stall even though 90% of the data has arrived. This is a property of TCP itself, not HTTP, and it affects every TCP-based protocol.
These two layers of HOL blocking compound each other. A single lost TCP segment on a connection carrying multiple HTTP responses can stall all of them. HTTP/2 solved application-level HOL blocking with multiplexed streams. HTTP/3 solved transport-level HOL blocking by replacing TCP with QUIC, which provides independent stream-level delivery over UDP.
The 6-connection-per-host limit
RFC 2616 (1999) recommended that clients open at most 2 persistent connections per host. Browsers ignored this almost immediately — the limit was too low for pages with dozens of resources. The industry settled on 6 connections per host, a number Chrome, Firefox, Safari, and Edge all use today. Chrome additionally caps total connections at 256.
Six connections per host, with HTTP/1.1’s one-response-at-a-time model per connection, meant a maximum of 6 resources in flight simultaneously from any single origin. For a page loading 80 resources, that’s a lot of waiting.
The web industry developed a toolkit of workarounds:
Domain sharding. Serve static assets from static1.example.com, static2.example.com, static3.example.com. Each subdomain gets its own pool of 6 connections. A page that was limited to 6 parallel downloads now has 18 or 24.
CSS sprites. Combine dozens of small icons into a single large image and use CSS background-position to display individual icons. One request instead of thirty.
JavaScript/CSS concatenation. Bundle all scripts into a single file and all stylesheets into a single file. Two requests instead of twenty.
Inlining. Embed small images as base64 data URIs in CSS. Embed critical CSS in <style> tags. Zero additional requests.
All of these techniques trade other costs (cache granularity, maintainability, wasted bytes) for fewer HTTP requests. They were necessary optimizations under HTTP/1.1 and they became anti-patterns under HTTP/2, where multiplexing eliminates the need to minimize request count.
Where HTTP/1.1 stands today
HTTP/1.1 is 29 years old. It has been surpassed by HTTP/2 (2015) and HTTP/3 (2022), but it remains the universal fallback.
+------------------+----------+--------+--------+ | Metric | HTTP/1.1 | HTTP/2 | HTTP/3 | +------------------+----------+--------+--------+ | Home page loads | ~22% | ~40% | ~38% | +------------------+----------+--------+--------+ | All requests | ~15% | ~37% | ~48% | +------------------+----------+--------+--------+ | Non-CDN requests | ~29% | ~55% | ~16% | +------------------+----------+--------+--------+
The non-CDN number is telling. CDNs adopted HTTP/2 and HTTP/3 early and aggressively. Behind the CDN — origin servers, internal APIs, legacy infrastructure — HTTP/1.1 is still the most common protocol. Many backend services never upgraded because the performance gains of HTTP/2 are less dramatic for low-latency, low-parallelism server-to-server communication.
HTTP/1.1 is also the protocol you’ll debug most often. curl defaults to it. Most proxy and load balancer logs are easiest to read in HTTP/1.1 format. When something goes wrong at the network layer, the plain-text request/response format is an advantage — you can read it in a packet capture without a decoder.
It is not going away.
HTTP/1.1 was not designed for the web we have. It was designed for the web that existed in 1997, and then carried a web a thousand times larger on its back for two decades. Every optimization trick of the 2000s and 2010s — sprites, concatenation, sharding, inlining — was a patch over its limitations. Understanding those limitations is the only way to understand why HTTP/2 and HTTP/3 exist, and what they actually fix.
See HTTP/2: Multiplexing and Binary Frames for what came next.