📝 HTTP/1.1

A protocol you can read, type, and telnet by hand — and the connection model that quietly capped web performance for a generation.

Human-Readable by Design

HTTP/1.1 is a plain-text, line-oriented protocol. Requests and responses are ASCII, delimited by CRLF, terminated by a blank line. This was a genuine feature: you can debug it with telnet, curl -v, or a packet capture and simply read the bytes.

$ printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' \
    | openssl s_client -quiet -connect example.com:443
The cost of readability: text parsing is ambiguous and slow. Header casing, whitespace, and line folding all became attack surface and interop headaches. This is a major reason HTTP/2 went binary.

Persistent Connections (keep-alive)

Before keep-alive: the original model opened a fresh TCP connection for every single request and closed it after the response. Each one paid a full TCP handshake (and later a TLS handshake) — brutal for a page pulling dozens of assets.

HTTP/1.1 made persistent connections the default: the socket stays open and is reused for the next request. Connection: close opts out.

sequenceDiagram participant C as Client participant S as Server Note over C,S: One TCP connection, reused C->>S: GET /a S->>C: 200 (a) C->>S: GET /b S->>C: 200 (b) C->>S: GET /c S->>C: 200 (c) Note over C,S: requests are strictly one-at-a-time
Win: the handshake cost is amortized across many requests. Catch: on a single connection, requests are still serialized — the client must get response A before it can read response B.

Pipelining — and Why It Failed

HTTP/1.1 tried to fix serialization with pipelining: send several requests back-to-back without waiting for each response. In theory, the server streams the responses in the same order.

GET /a HTTP/1.1
Host: x

GET /b HTTP/1.1
Host: x

GET /c HTTP/1.1          ← all three sent without waiting
Fatal flaw — responses must return in request order. If /a is a slow database query and /b, /c are instant, the client still can't receive B and C until A is done. This is head-of-line blocking at the application layer.

Combined with buggy proxies that mis-ordered or corrupted pipelined responses, browsers concluded it wasn't worth it. Pipelining was effectively never enabled by default anywhere — it's a dead feature you should assume is off.

The Real-World Workaround: Parallel Connections

With one connection stuck doing one request at a time, browsers bought concurrency the blunt way: open several TCP connections to the same host and spread requests across them. The de-facto cap settled at roughly six connections per origin.

flowchart LR B["Browser"] B --> C1["conn 1 → GET /a"] B --> C2["conn 2 → GET /b"] B --> C3["conn 3 → GET /c"] B --> C4["conn 4 → GET /d"] B --> C5["conn 5 → GET /e"] B --> C6["conn 6 → GET /f"] C7["/g, /h, /i … wait in queue"]
Six is not many. A modern page can reference 100+ resources. Beyond six, requests queue on the client. Each extra connection also means another handshake, another congestion-control slow start, and more memory on the server.

Domain Sharding (the hack on top of the hack)

The six-per-origin limit is per hostname. So developers split assets across fake subdomains — static1.example.com, static2.example.com — to unlock 6× more connections each.

Real-world: sharding worked, but it multiplied DNS lookups, TCP handshakes, and TLS negotiations, and it fought TCP congestion control (each connection ramps up independently). It's an anti-pattern under HTTP/2, which multiplexes everything over one connection — sharding there actively hurts.

Framing: How the Body Ends

Since one connection carries many messages, the receiver must know where each body stops. Two ways:

MechanismHow it delimitsUse
Content-Length: NRead exactly N bytesKnown-size bodies
Transfer-Encoding: chunkedSize-prefixed chunks, a 0 chunk ends itStreaming / unknown length
HTTP/1.1 200 OK
Transfer-Encoding: chunked

1b
{"partial":"first chunk"}
0

Why this matters: disagreements between Content-Length and chunked framing are the root of request smuggling attacks. Text framing is fragile — another motivation for HTTP/2's strict binary frames.

Key Takeaways

FeatureHTTP/1.1 reality
EncodingPlain text, CRLF-delimited
Connection reusekeep-alive by default
Concurrency on one connNone — one request at a time
PipeliningSpecified, broken, disabled everywhere
Real concurrency~6 parallel connections per origin
Header overheadFull text headers repeated every request
Bottom line: HTTP/1.1 is simple, debuggable, and still everywhere — but its one-request-per-connection model forced ugly workarounds (parallel connections, sharding, concatenating assets). HTTP/2 was built to delete all of them at once.