Connection: keep-alive and the Keep-Alive header (tested)

In HTTP/1.1 every connection is persistent by default: after a response, the TCP socket stays open for the next request unless one side sends Connection: close. Connection: keep-alive mostly matters for HTTP/1.0. Keep-Alive: timeout=5 is only a hint about how long the server will keep an idle socket. We measured what Node.js, curl and Python actually send, how many connections they open, when an idle socket really closes, and how to reproduce the ECONNRESET you get when a client reuses a socket the server is closing.

1. What Node.js sends by default

A plain http.createServer((req, res) => res.end('ok\n')) on Node.js 24.21.0, requested with curl:

HTTP/1.1 200 OK
Date: Fri, 02 Oct 2026 02:07:38 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 3

When the request had Connection: close (curl -H "Connection: close") or was HTTP/1.0 (curl --http1.0), the response said Connection: close, had no Keep-Alive header, and the server closed the socket right after it. The server's defaults, read from the server object:

PropertyDefault in Node.js 24.21.0
server.keepAliveTimeout5000 ms
server.keepAliveTimeoutBuffer1000 ms
server.headersTimeout60000 ms
server.requestTimeout300000 ms
server.timeout0 (off)
server.maxRequestsPerSocket0 (unlimited)

2. When an idle socket really closes

We sent one request over a raw TCP socket, then timed how long it took the server to close the idle connection:

keepAliveTimeoutHeader sentServer closed the idle socket after
5000 (default)Keep-Alive: timeout=56,007 ms
2000Keep-Alive: timeout=23,005 ms
500Keep-Alive: timeout=0not timed (clients never reused it, see section 4)
0no Keep-Alive header, still Connection: keep-alivenever: still open after 12 s

Node.js advertises keepAliveTimeout but waits an extra keepAliveTimeoutBuffer (1 s) before it closes. That gives clients a margin. Two traps: a value under 1000 ms is rounded down to timeout=0 in the header, and keepAliveTimeout = 0 turns idle closing off entirely.

headersTimeout is a different thing. It limits how long a client can take to send the request headers. With http.createServer({ headersTimeout: 3000, connectionsCheckingInterval: 500 }), a client that sent half a request got HTTP/1.1 408 Request Timeout after 3,008 ms. With the defaults (60 s, checked every 30 s by default), a half-sent request was still open after 8 s, as expected. For limits on header size, see HTTP header size limits.

3. Which clients reuse connections

Each client made 5 sequential requests to the same Node.js server. We counted new TCP connections with server.on('connection'):

ClientSends Connection:Connections for 5 requests
curl 8.14.1, 5 URLs in one command (http://host/[1-5])(none)1
curl 8.14.1, 5 separate commands(none)5
curl with -H "Connection: close", 2 URLsclose2 (one per URL)
Node.js fetch()keep-alive2 (requests alternated between the two)
Node.js http.get (global agent)keep-alive1
Node.js new http.Agent()close5
Node.js new http.Agent({ keepAlive: true })keep-alive1
Node.js agent: falseclose5
Python requests.get() x5keep-alive5
Python requests.Session().get() x5keep-alive1
Python urllib.request.urlopenclose5

What this means:

4. How long each client keeps an idle socket

We sent a request, waited, then sent another, against the default Node.js server (Keep-Alive: timeout=5, real close at 6 s) unless noted:

ClientReused afterNew connection after
fetch()2.9 s3.1 s (hint minus about 2 s)
http.globalAgent (has timeout: 5000), tested against a timeout=2 server0.9 s1.1 s (hint minus 1 s)
new http.Agent({ keepAlive: true }), no timeout5.9 s6.1 s (only after the server closed it)
new http.Agent({ keepAlive: true, timeout: 30000 })3.9 s4.1 s

So fetch and agents with a timeout read the Keep-Alive: timeout=N hint and drop the socket early. A keep-alive agent without timeout ignored the hint and kept the socket until the server's FIN arrived. Against a server sending timeout=0, fetch and the keep-alive agent opened a new connection for every request, even 100 ms apart.

5. Reproducing the ECONNRESET race

This is the classic keep-alive bug: the client sends a request on a pooled socket at the same moment the server closes it. The server resets the connection and the client gets ECONNRESET or "socket hang up". We set the server to keepAliveTimeout = 2000 and keepAliveTimeoutBuffer = 0, used new http.Agent({ keepAlive: true }), and waited 1,985 to 2,015 ms (1 ms steps) between two requests:

WaitResult
1,985 to 1,997 msreused (13 of 13)
1,998 to 2,000 msECONNRESET (3 of 3)
2,001 to 2,015 msnew connection (15 of 15)

With the default 1 s buffer the window just moved to 2,997 to 3,001 ms (5 resets in 31 tries). The buffer only helps clients that read the hint, and this agent did not. The same sweep with new http.Agent({ keepAlive: true, timeout: 1500 }) gave 21 of 21 new connections and no errors. Rare in a test, but at thousands of requests per second with the right idle gaps, it shows up as occasional 502s or resets.

The fix is always the same: the client must give up an idle socket before the server does.

6. HTTP/2 and HTTP/3 forbid these headers

RFC 9113 says HTTP/2 must not use connection-specific headers (Connection, Keep-Alive, Transfer-Encoding, Upgrade, Proxy-Connection). One connection carries many streams, so there is nothing to keep alive per request. In Node.js 24.21.0 http2:

If a tool shows Connection: keep-alive on an HTTP/2 response, it is showing HTTP/1.1 headers or making them up. Related: Transfer-Encoding: chunked vs Content-Length, which is also gone in HTTP/2.

7. Checklist

  1. Check the response: curl -sI http://host/ shows Connection and Keep-Alive. curl -v url1 url2 shows whether the second request said Re-using existing.
  2. In Python use a requests.Session(). In Node.js use the global agent, fetch, or an agent with keepAlive: true and a timeout.
  3. Make every client idle timeout shorter than the server's: browser and app to load balancer, load balancer to app.
  4. Leave the Node.js keepAliveTimeout above 1000 ms so the header is not timeout=0, and never 0 on a public server.
  5. Do not set Connection or Keep-Alive on HTTP/2 responses.

Frequently asked questions

Do I need to send Connection: keep-alive in HTTP/1.1?

No. HTTP/1.1 connections are persistent by default. curl 8.14.1 sends no Connection header and still reused one connection for 5 URLs in one command. The header mainly matters for HTTP/1.0, where Node.js 24 answered with Connection: close.

What does Keep-Alive: timeout=5 mean?

It is the server's hint that it will close an idle connection after about 5 seconds. Node.js 24.21.0 sends it from server.keepAliveTimeout (5000 ms by default) but actually closed the idle socket after 6,007 ms because of its 1 s keepAliveTimeoutBuffer.

Why does requests.get not reuse connections?

Each requests.get() call creates and closes its own session. Five calls opened 5 TCP connections in our test; five calls on one requests.Session() opened 1.

Why do I get ECONNRESET or socket hang up on keep-alive connections?

The client reused a pooled socket at the moment the server was closing it. We reproduced it with a Node.js keep-alive agent and a 2 s server timeout: waits of 1,998 to 2,000 ms gave ECONNRESET. Make the client's idle timeout shorter than the server's, for example http.Agent({ keepAlive: true, timeout: 30000 }), or raise server.keepAliveTimeout above your load balancer's idle timeout.

Can I use Connection: keep-alive with HTTP/2?

No. RFC 9113 forbids connection-specific headers. Node.js 24 http2 threw ERR_HTTP2_INVALID_CONNECTION_HEADERS when we set connection or keep-alive, and curl did not send them over HTTP/2.

Written with AI assistance. Measured on 2026-10-02 with Node.js 24.21.0 (http.createServer and http2 on localhost:8987-8988, built-in fetch, http.Agent), curl 8.14.1, Python 3.13.5 with requests 2.32.3 and urllib. We counted connections with the server's 'connection' event and timed idle closes with a raw TCP client. nginx and cloud load balancer timeouts are quoted from their documentation, not measured. PlainHeaders is our extension; Node.js, curl and Python are free.