Every time your browser loads a resource from a server, it relies on a TCP connection. Understanding how TCP connections are established — and how Keep-Alive reuses them — is fundamental to understanding web performance.
TCP connections are expensive
Before your browser can send a single byte of HTTP data, it must complete a three-way handshake with the server:
- SYN — The client sends a synchronization packet to the server.
- SYN-ACK — The server acknowledges and responds with its own synchronization.
- ACK — The client acknowledges the server’s response.
Only after this exchange can the actual HTTP request be sent. On a typical broadband connection this takes 20–100ms. On a mobile network or cross-continental connection, it can easily reach 200–500ms. When you consider that a modern web page makes dozens or hundreds of HTTP requests, the cumulative cost of repeatedly opening and closing connections becomes significant.
Client Server
| |
|-------- SYN ----------------> |
| |
| <------- SYN-ACK ------------ |
| |
|-------- ACK ----------------> |
| |
|===== Connection Ready ======= |
| |
|-------- GET /index.html ----> |
| |
| <------- 200 OK + body ------- |
What Keep-Alive means
Keep-Alive (also called a persistent connection) allows multiple HTTP requests to reuse the same TCP connection rather than tearing it down and re-establishing it each time. Instead of paying the three-way handshake cost for every resource, you pay it once and amortize it across all requests on that connection.
Under HTTP/1.0, connections were closed by default after each response. HTTP/1.1 made persistent connections the default, and the Connection: keep-alive header became the mechanism to negotiate them explicitly.
When your browser connects to a server, it sends a request like this:
GET /style.css HTTP/1.1
Host: example.com
Connection: keep-alive
Keep-Alive: timeout=5, max=1000
The server responds with:
HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 4823
Connection: keep-alive
Keep-Alive: timeout=5, max=1000
The two key parameters in the Keep-Alive header are:
timeout — How many seconds the connection can remain idle before the server closes it. In this example, timeout=5 means the server will close an idle connection after 5 seconds.
max — The maximum number of requests allowed on this connection before it is closed. max=1000 means the connection can serve up to 1000 requests before being torn down.
To explicitly close a connection after a response, either party can send:
Set your server’s keep-alive timeout to be slightly longer than the interval
at which your clients typically make follow-up requests. Too short and you
lose the benefit; too long and idle connections waste server file descriptors
and memory.
HTTP/1.1 persistent connections vs. HTTP/2 multiplexing
Keep-Alive under HTTP/1.1 is a significant improvement over HTTP/1.0, but it still has a fundamental limitation: requests on a connection are still sequential. The browser must wait for the current response before sending the next request on that connection (head-of-line blocking). This is why browsers open 6 parallel connections per origin — to achieve concurrency.
HTTP/2 takes a different approach entirely. Rather than reusing a connection for sequential requests, HTTP/2 multiplexes multiple request streams over a single connection simultaneously:
HTTP/1.1 Keep-Alive (3 connections):
Conn 1: [req1]---[resp1][req4]---[resp4]
Conn 2: [req2]---[resp2][req5]---[resp5]
Conn 3: [req3]---[resp3][req6]---[resp6]
HTTP/2 (1 connection, multiplexed):
Conn: [req1][req2][req3][req4][req5][req6]
[resp3][resp1][resp5][resp2][resp4][resp6] (any order)
If you’re serving over HTTP/2, you don’t need to tune Keep-Alive for
concurrency — a single persistent connection handles it. Focus instead on
ensuring your TLS and connection setup is fast, since that one connection
carries all traffic.
Performance implications
Reduce connection overhead in HTTP/1.1
If you’re serving over HTTP/1.1 (for example, for clients that don’t support HTTP/2), persistent connections are essential. Without them, every image, stylesheet, and script pays the full handshake cost. With a well-configured keep-alive, subsequent requests on the same connection skip the handshake entirely.
// Node.js (http module): enable keep-alive on an outgoing agent
const http = require('http');
const agent = new http.Agent({
keepAlive: true,
maxSockets: 10,
keepAliveMsecs: 3000, // Send TCP keep-alive probes every 3 seconds
});
http.get({ hostname: 'example.com', path: '/', agent }, (res) => {
// Connection will be reused for subsequent requests
});
TCP-level keep-alive vs. HTTP-level Keep-Alive
Note the distinction between two related but separate mechanisms:
- HTTP Keep-Alive (covered above): reuses a TCP connection across multiple HTTP requests. Configured via HTTP headers.
- TCP keep-alive: a lower-level mechanism where the OS sends periodic probe packets on idle connections to detect if the remote side has disappeared. Configured at the socket/OS level, not via HTTP headers.
Both are often described with the same term, but they operate at different layers of the network stack.
TCP keep-alive probes are primarily a reliability mechanism — they ensure
long-lived idle connections aren’t silently dropped by NAT devices or
firewalls. HTTP Keep-Alive is a performance mechanism for connection reuse.
Nginx configuration example
Here is a minimal Nginx configuration that enables HTTP keep-alive with sensible defaults:
http {
keepalive_timeout 65; # Close idle connections after 65 seconds
keepalive_requests 1000; # Maximum requests per connection
server {
listen 443 ssl http2;
server_name example.com;
# ... SSL and location config
}
}
Enable HTTP/2 (http2 on the listen directive) alongside keep-alive. HTTP/2
clients will automatically benefit from multiplexing while HTTP/1.1 clients
fall back to persistent connections.
Summary
Keep-Alive is one of the most impactful and lowest-effort performance improvements available. By reusing TCP connections, you eliminate repeated handshake latency for every resource on the page. Under HTTP/1.1, this means fewer connection round-trips across many parallel connections. Under HTTP/2, it means a single connection carries all traffic with full concurrency — making Keep-Alive less of a tuning concern and more of a baseline assumption. Either way, you should ensure your server is configured to support persistent connections and that your clients are taking advantage of them.