How HTTP Works

Request and response header

< Keep-Alive: timeout=5, max=1000

Keep-Alive Header

Learn how the Keep-Alive header controls HTTP connection persistence and reuse. Reduce latency and improve performance by avoiding repeated TCP handshakes.

Reviewed 5 min readintermediate2 sourcesTry itMarkdown
Direction
Request and response
Category
Connection management
Spec
RFC 2068 §19.7.1.1 (legacy)
Status
Legacy: HTTP/1.1 only; not defined in RFC 9110 or 9112
JS can set it
No: forbidden header name
Hop-by-hop
Yes: not forwarded by proxies
On this page

TL;DR: Keep-Alive advertises HTTP/1.x connection hints such as an idle timeout in seconds. It does not enable persistence or enforce socket limits. HTTP/1.1 already permits reuse; HTTP/2 and HTTP/3 forbid this header.

The header lets a sender hint at how it intends to use a persistent connection. Actual limits come from server configuration, not from a client choosing a larger timeout. MDN documents timeout and max.

How Keep-Alive Works

An illustrative response with an empty body:

HTTP/1.1 200 OK
Connection: keep-alive
Keep-Alive: timeout=5, max=100
Content-Length: 0

timeout refers to idle time between requests, not time allowed to generate a response. max advertises a request-count limit; do not assume all clients enforce it.

Syntax

Keep-Alive: timeout=<seconds>, max=<requests>

HTTP/1.0 vs HTTP/1.1

HTTP/1.0 implementations can negotiate the historical keep-alive extension. HTTP/1.1 persistence does not require Connection: keep-alive or Keep-Alive. Reuse requires unambiguous message framing and consumption of the complete response body. See RFC 9112 §9.3.

Closing Connections

Connection: close signals that HTTP/1.1 reuse ends after the current response. A peer can also close an idle connection without sending another response; clients must handle that possibility.

Server Configuration Examples

nginx

Place these deliberately chosen settings in an existing http, server, or location block:

keepalive_timeout 30s 30s;
keepalive_requests 1000;

The first timeout controls the client socket; the second sets the advertised Keep-Alive: timeout=30. They can differ. The documented nginx defaults are keepalive_timeout 75s and keepalive_requests 1000. See the core module directives. These are client-facing settings, not an upstream connection-pool configuration.

Apache

This explicitly configures persistence, request count, and idle time:

KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5

Apache documents these directives in its core module.

Node.js

Configure socket behavior directly rather than promising limits in a handwritten header:

import http from 'node:http'

const server = http.createServer((req, res) => res.end('hello'))
server.keepAliveTimeout = 5000 // milliseconds; chosen idle timeout
server.maxRequestsPerSocket = 100 // chosen request limit
server.listen(3000)

maxHeadersCount limits header fields, not requests. maxRequestsPerSocket signals Connection: close at the limit; further pipelined requests receive 503.

Behind a load balancer, make the backend’s idle timeout longer than the balancer’s. AWS ALB defaults to 60 seconds; if the app closes idle connections first, the balancer can send a request down a socket the app just closed and return a 502. For Node behind an ALB, server.keepAliveTimeout = 65000 is the usual setting. The nginx 502 guide walks through this race.

nginx upstream pooling

The downstream idle timer does not create an upstream pool. For a backend on the same machine, put this inside nginx’s http block:

upstream app {
    server 127.0.0.1:3000;
    keepalive 16;
}
server {
    listen 8080;
    server_name localhost;
    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Here keepalive 16 caches up to 16 idle upstream connections per worker. It does not limit all connections nginx may open to the backend. Clearing Connection avoids nginx sending an upstream close; explicitly selecting HTTP/1.1 makes the intended behavior independent of configuration defaults. See the upstream keepalive directive.

Test through port 8080 while logging connections at the backend. A downstream Keep-Alive response field cannot tell you whether this upstream cache is in use.

Client Implementation

Browser scripts cannot set Connection or Keep-Alive: both are forbidden request headers. Fetch’s keepalive option concerns a request outliving its page, not configuring HTTP idle timeouts.

In Node’s http.Agent, keepAliveMsecs controls the initial delay for TCP keepalive probes. It is not an HTTP connection-pool idle timeout. See the Agent options.

Node HTTP client

An explicit Agent makes the pool lifetime clear. This local diagnostic consumes both response bodies before destroying it:

import http from 'node:http'

const agent = new http.Agent({ keepAlive: true, maxSockets: 4 })
const get = () => new Promise((resolve, reject) => {
  http.get('http://127.0.0.1:3000/', { agent }, (res) => {
    res.resume()
    res.on('end', resolve)
    res.on('error', reject)
  }).on('error', reject)
})
try {
  await get()
  await get()
} finally {
  agent.destroy()
}

Save as client.mjs and run while the local server above is listening. The four-socket limit is a chosen per-host capacity; it is not a timeout. Destroying the Agent closes its managed sockets when this short-lived client has finished.

Python requests

A Session retains a pool across requests. This complete example consumes the response bodies and closes the Session afterward:

import requests

with requests.Session() as session:
    for path in ('/', '/health'):
        response = session.get('http://127.0.0.1:3000' + path, timeout=5)
        response.raise_for_status()
        print(path, response.text)

The five-second Requests timeout is an explicit diagnostic setting, not the server’s Keep-Alive timeout. With stream=True, consume the body or close the Response before expecting the connection to return to the pool. Requests documents this requirement.

curl

Run both transfers in one process to make reuse possible:

curl --http1.1 -v http://127.0.0.1:3000/ http://127.0.0.1:3000/ -o /dev/null
curl --http1.1 -v -H 'Connection: close' http://127.0.0.1:3000/ -o /dev/null

curl’s --no-keepalive concerns TCP keepalive probes; it does not disable HTTP persistent connections. The HTTP/1.1 Connection: close field is the relevant comparison. See curl’s option definition.

Performance Benefits

Reusing a connection avoids establishing a new TCP connection for each request. TCP establishes a connection with a three-message handshake, not three round trips. The latency saved depends on the connection and workload.

Set socket limits where connections are managed. A response header alone cannot turn an upstream pool on or fix a timeout mismatch.

Common Issues

nginx proxy_connect_timeout, proxy_send_timeout, and proxy_read_timeout measure upstream connection establishment or I/O waits. They are not interchangeable with downstream keepalive_timeout. Check the proxy module definitions before comparing values.

A Node client can encounter ECONNRESET if the server closes an idle socket as the client tries to reuse it. Log request.reusedSocket alongside the error, as in Node’s documented retry example. Retry only when the operation permits it; blindly retrying a write can repeat a side effect.

For observation, use server.getConnections() rather than an assumed server.connections counter:

server.getConnections((error, count) => {
  if (error) console.error(error)
  else console.log('open connections', count)
})

This snippet applies to an existing Node server. The count includes its current connections; it does not identify which are idle. The net.Server API documents the callback.

HTTP/2 and HTTP/3

Keep-Alive is a forbidden connection-specific field in HTTP/2 and HTTP/3. Omit it when speaking those protocols.

Security Considerations

An idle timeout after a completed response does not limit how long a client can spend sending request headers. Node exposes separate headersTimeout and requestTimeout settings; nginx exposes client_header_timeout.

Frequently asked questions

What is the Keep-Alive header?

Keep-Alive provides parameters for persistent connections: timeout (seconds to keep open) and max (maximum requests). Used with Connection: keep-alive.

Is Keep-Alive still relevant?

It can advertise connection hints in HTTP/1.x. It is forbidden in HTTP/2 and HTTP/3, which manage connections at the protocol level.

What does timeout parameter mean?

timeout=5 advertises an idle timeout of five seconds between requests. It is not a request-processing deadline or a guarantee against earlier closure.

What does max parameter mean?

max advertises a maximum request count for the connection. Enforcement and client handling depend on the implementation; configure the actual server limit separately.

Sources

  1. MDN Web Docs: Keep-Alivedeveloper.mozilla.org
  2. RFC 9112: Keep-Aliverfc-editor.org

Keep going

Browse /search