Step 1 of 5 · Reading · ~4 min
Read
Concurrency & Persistence
Keep-Alive Connections
HTTP/1.0 opened a TCP connection, sent one request, read one response, and closed. Every image, stylesheet and script on a page paid for its own three-way handshake — and on a 100 ms link that is 100 ms of silence before a single byte of your CSS moves, repeated forty times. HTTP/1.1 fixed it by inverting the default: the connection stays open until somebody says otherwise. Everything below follows from that inversion, including the ways it goes wrong.
What reuse actually buys
A new TCP connection costs one round trip before any data (SYN, SYN-ACK, ACK) and, over TLS, one or two more for the handshake. It also costs throughput: TCP starts every connection in slow start with a small congestion window, so a fresh socket sends a few packets and waits, again and again, until it learns the path. A reused connection has already paid for all of that. This is why keep-alive is not a micro-optimisation — the second request on a warm socket can beat the first by an order of magnitude, and the gap widens the further away the client is.
The two signals, and who they bind
Connection: keep-alive # HTTP/1.0: "please stay open". HTTP/1.1: redundant but legal.
Connection: close # either version: this is the last message on this socket
Connection is a hop-by-hop header: it describes this one TCP link, not the request's journey. A proxy in the middle must consume it and decide its own upstream policy rather than forwarding it — the same header can say close toward the client and mean nothing to the origin. Header names are case-insensitive and so is the value in practice: Close, close and CLOSE all mean the same thing, and a parser that only recognises the lowercase spelling will hold sockets open that the client already abandoned.
The whole decision is a two-by-two, and your exercise is exactly this table:
| Request version | No Connection header | Connection: close | Connection: keep-alive |
|---|---|---|---|
| HTTP/1.1 | keep open (the default) | close | keep open |
| HTTP/1.0 | close (the default) | close | keep open |
Read it as one sentence: the version supplies the default and the header overrides it.
The server loop
conn = accept(socket)
while True:
request = parse(conn) # None => the peer closed or sent garbage
if request is None: break
response = handle(request)
send(conn, response)
if wants_close(request): break # the table above
if requests_served > MAX: break # bound the socket's lifetime
close(conn)
Two lines there are load-bearing beyond the obvious. parse must consume exactly the body the framing headers describe — this is where the Content-Length and chunked discipline from the body lesson stops being pedantry, because one byte over-read or under-read leaves the stream misaligned and the next request on this socket parses as nonsense. A single-shot server hides framing bugs; a keep-alive server exposes every one of them.
And the loop must have an idle timeout (15–60 s is typical) plus a cap on requests served, because a client that vanishes without a FIN leaves you holding a socket and a file descriptor forever. Enough of those and accept() starts failing with EMFILE while the process looks perfectly healthy. The timeout is not politeness; it is the only thing standing between you and resource exhaustion by clients who did nothing malicious at all.
Pipelining, and what replaced it
HTTP/1.1 also permits pipelining — firing several requests without waiting for the responses in between. It is essentially dead in practice: responses must come back in request order, so one slow handler stalls everything queued behind it (head-of-line blocking), and buggy intermediaries mangled it often enough that browsers disabled it. HTTP/2 solved the real problem instead, by multiplexing independent streams over one connection so a slow response blocks only itself. Keep-alive is the idea that survived; pipelining is the cautionary tale about the ordering constraint that came with it.
Your exercise: Keep-Alive State Machine
Version-and-header pairs in, KEEP or CLOSE out — the table above, made executable. The ladder walks it cell by cell: an HTTP/1.1 request with no header (keep), the same with close (close), an HTTP/1.0 request with no header (close), HTTP/1.0 with keep-alive (keep), and then the case-insensitive spellings — Close, KEEP-ALIVE — that catch a parser comparing raw strings. It is ten lines of decision, and it is the decision that every connection your server ever accepts will run exactly once per request.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…