Skip to content
Lesson 7 of 16

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 versionNo Connection headerConnection: closeConnection: keep-alive
HTTP/1.1keep open (the default)closekeep open
HTTP/1.0close (the default)closekeep 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.

Up nextConcurrent ConnectionsConcurrency & Persistence

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…