TRT
Sign in
Networking

HTTP Keep-Alive and Connection Reuse: What the Machine Actually Does

Why HTTP/1.1 needs Connection: keep-alive, what the server timeout does, and how curl and ss reveal whether your connections are being reused or rebuilt.

The Runtime Theory Team8 min read#http#keep-alive#connections#curl#networking

Every HTTP request is expensive before a single byte of payload moves: a cold connection costs one RTT for the TCP handshake, one for TLS, and then the request itself. On a 40 ms RTT link, that's 80 ms of pure overhead before the server even reads your GET. Keep-alive exists to amortize that cost across many requests. But it's a timer-driven bargain between two machines, and it works exactly as well as the settings on both sides.

The three-way handshake you keep paying for

Look at any fresh connection from the client side:

bash
$ curl -v https://api.example.com/resource 2>&1 | grep -E "TCP|TLS|Re-using"
* Connected to api.example.com (104.21.62.13) port 443
* ALPN: offers h2,http/1.1
* TLS 1.3 ...
* Connected, TLS handshake completed in 0.04s
> GET /resource HTTP/1.1

The SYN, SYN-ACK, ACK, then the TLS handshake — on an RTT of 40 ms that's roughly 80 ms before the request line leaves the machine. Now do it for 40 requests (a typical page load) and the connection setup alone is 3.2 seconds. That's what keep-alive eliminates: the second request on the same connection costs zero RTTs of setup. It is one of the cheapest performance wins in networking, because it doesn't make anything faster — it stops re-paying for the same handshake.

What the server actually does

Keep-alive on the server is not a feature — it's a policy about idle time. The server holds the connection open, with the TCP socket parked, waiting for the next request. But it cannot wait forever: each parked socket consumes a file descriptor, kernel memory, and a slot in the accept queue. So every server runs a clock. On nginx that's keepalive_timeout, on Apache KeepAliveTimeout, on Node the socket's setTimeout:

bash
keepalive_timeout 75s;        # how long to hold an idle connection
keepalive_requests 1000;      # max requests per connection, then close

These two knobs define the contract. keepalive_timeout answers "how long do I hold an idle socket before I close it?" and keepalive_requests answers "how many requests does one connection get before I recycle it?" — the second exists because a very long-lived connection accumulates state (head-of-line risk, balancer skew, and, on HTTP/1.1, sequential coupling).

The four-way negotiation you never see

Whether a connection gets reused is decided by four independent clocks:

text
client idle timeout  ──┐
server keepalive      ──┼──> connection closes when ANY timer fires
client app close      ──┤
server max requests   ──┘

If the server's timeout is 75 s and the client's pool timeout is 60 s, the client wins: it reaps the socket and starts a new handshake at exactly 60 s of idle. If the client's timeout is longer, it gets a Connection: close and a FIN mid-use — a one-time latency spike. There is no negotiation message; each side closes when its own timer fires, and the other side discovers it on the next write. That's why "connection reset" errors in a pool almost always mean timer mismatch, not a server crash.

Observe it yourself

bash
$ ss -tan | grep 443
ESTAB 0 0 192.168.1.20:51234 104.21.62.13:443
$ curl -o /dev/null -s -w "total: %{time_total}s\n" \
    https://api.example.com/ping
total: 0.032s        # cold: handshake + TLS + request
$ curl -o /dev/null -s -w "total: %{time_total}s\n" \
    https://api.example.com/ping
total: 0.004s        # warm: connection reused, ~no setup cost

curl reuses connections automatically within a single invocation and shows Re-using existing connection when it does. The gap between the first and second number — 0.032 s vs 0.004 s — is the entire value of keep-alive. In ss output you can watch the socket transition through ESTAB (idle but parked) to FIN-WAIT-2 / TIME-WAIT when the server's timer fires.

Why HTTP/2 and HTTP/3 changed the terms

Keep-alive solves connection setup cost, but HTTP/1.1 still processes one request at a time per connection — the next request waits for the previous response (head-of-line blocking). HTTP/2 keeps the single reused connection but multiplexes many requests over it; HTTP/3 keeps multiplexing and also survives network changes via QUIC. The reused connection is the foundation of both — keep-alive is not obsolete, it's assumed.

The mental model

  • Keep-alive eliminates a fixed per-request tax: 2 RTTs on HTTPS, paid once per connection.
  • It's a contract of timers — client pool timeout must be shorter than server keepalive_timeout, or you'll reap idle sockets and rebuild them.
  • One socket in ss, many requests across it: that's the whole trick.
  • If your p95 shows a bimodal distribution — most requests fast, a few much slower — you're almost certainly watching pool rebuilds: new handshake, new slow start, same server, new timer.

Production cost model and latency numbers

To apply HTTP Keep-Alive and Connection Reuse: What the Machine Actually Does effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:

Operation / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, TCP/TLS framing, tail queueing ($L = \lambda W$)Connection pooling, deadline propagation, and p95 hedged requests

Failure modes, debugging, and operational checklist

In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on HTTP Keep-Alive and Connection Reuse: What the Machine Actually Does, walk through three verification questions:

  1. What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save