A protocol you can read, type, and telnet by hand — and the connection model that quietly capped web performance for a generation.
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
HTTP/1.1 made persistent connections the default: the socket stays open and is reused
for the next request. Connection: close opts out.
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
/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.
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.
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.
Since one connection carries many messages, the receiver must know where each body stops. Two ways:
| Mechanism | How it delimits | Use |
|---|---|---|
Content-Length: N | Read exactly N bytes | Known-size bodies |
Transfer-Encoding: chunked | Size-prefixed chunks, a 0 chunk ends it | Streaming / unknown length |
HTTP/1.1 200 OK
Transfer-Encoding: chunked
1b
{"partial":"first chunk"}
0
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.
| Feature | HTTP/1.1 reality |
|---|---|
| Encoding | Plain text, CRLF-delimited |
| Connection reuse | keep-alive by default |
| Concurrency on one conn | None — one request at a time |
| Pipelining | Specified, broken, disabled everywhere |
| Real concurrency | ~6 parallel connections per origin |
| Header overhead | Full text headers repeated every request |