# 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.

Source: https://howhttpworks.com/headers/keep-alive
Last reviewed: 2026-10-05

> **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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Keep-Alive) documents `timeout` and `max`.

## How Keep-Alive Works

An illustrative response with an empty body:

```http
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

```text
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](https://www.rfc-editor.org/rfc/rfc9112#section-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:

```nginx
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](https://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_timeout). These are client-facing settings, not an upstream connection-pool configuration.

### Apache

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

```apache
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
```

Apache documents these directives in its [core module](https://httpd.apache.org/docs/2.4/mod/core.html#keepalive).

### Node.js

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

```javascript
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`](https://nodejs.org/api/http.html#servermaxheaderscount) limits header fields, not requests. [`maxRequestsPerSocket`](https://nodejs.org/api/http.html#servermaxrequestspersocket) 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](https://howhttpworks.com/debug/nginx-502-bad-gateway) 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:

```nginx
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](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive).

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](https://fetch.spec.whatwg.org/#forbidden-request-header). 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](https://nodejs.org/api/http.html#new-agentoptions).

### Node HTTP client

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

```javascript
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:

```python
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](https://requests.readthedocs.io/en/latest/user/advanced/#keep-alive).

### curl

Run both transfers in one process to make reuse possible:

```bash
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](https://curl.se/docs/manpage.html#--no-keepalive).

## Performance Benefits

Reusing a connection avoids establishing a new TCP connection for each request. TCP establishes a connection with a [three-message handshake](https://www.rfc-editor.org/rfc/rfc9293#section-3.5), 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](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout) 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](https://nodejs.org/api/http.html#requestreusedsocket). 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:

```javascript
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](https://nodejs.org/api/net.html#servergetconnectionscallback) documents the callback.

## HTTP/2 and HTTP/3

`Keep-Alive` is a forbidden connection-specific field in [HTTP/2](https://www.rfc-editor.org/rfc/rfc9113#section-8.2.2) and [HTTP/3](https://www.rfc-editor.org/rfc/rfc9114#section-4.2). 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`.

## Related Headers

- [Connection](https://howhttpworks.com/headers/connection)
- [Upgrade](https://howhttpworks.com/headers/upgrade)
- [Content-Length](https://howhttpworks.com/headers/content-length)
