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:
| Property | Default in Node.js 24.21.0 |
|---|---|
server.keepAliveTimeout | 5000 ms |
server.keepAliveTimeoutBuffer | 1000 ms |
server.headersTimeout | 60000 ms |
server.requestTimeout | 300000 ms |
server.timeout | 0 (off) |
server.maxRequestsPerSocket | 0 (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:
keepAliveTimeout | Header sent | Server closed the idle socket after |
|---|---|---|
| 5000 (default) | Keep-Alive: timeout=5 | 6,007 ms |
| 2000 | Keep-Alive: timeout=2 | 3,005 ms |
| 500 | Keep-Alive: timeout=0 | not timed (clients never reused it, see section 4) |
| 0 | no Keep-Alive header, still Connection: keep-alive | never: 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'):
| Client | Sends 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 URLs | close | 2 (one per URL) |
Node.js fetch() | keep-alive | 2 (requests alternated between the two) |
Node.js http.get (global agent) | keep-alive | 1 |
Node.js new http.Agent() | close | 5 |
Node.js new http.Agent({ keepAlive: true }) | keep-alive | 1 |
Node.js agent: false | close | 5 |
Python requests.get() x5 | keep-alive | 5 |
Python requests.Session().get() x5 | keep-alive | 1 |
Python urllib.request.urlopen | close | 5 |
What this means:
- Reuse lives inside one process and one connection pool. Five curl commands always open five connections, whatever the headers say.
curl -vprintsRe-using existing http: connection with host 127.0.0.1for the second and later URLs in one command. requests.get()sendsConnection: keep-alivebut creates and discards a new pool on every call, so nothing is reused. Use arequests.Session().- In Node.js 24,
http.globalAgent.keepAliveistrue(options:keepAlive: true, scheduling: 'lifo', timeout: 5000). Anew http.Agent()you create yourself still defaults tokeepAlive: false. fetch()back-to-back opened a second socket because the first was not yet back in the pool. With 50 ms between requests, 4 of 5 used the same socket.
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:
| Client | Reused after | New connection after |
|---|---|---|
fetch() | 2.9 s | 3.1 s (hint minus about 2 s) |
http.globalAgent (has timeout: 5000), tested against a timeout=2 server | 0.9 s | 1.1 s (hint minus 1 s) |
new http.Agent({ keepAlive: true }), no timeout | 5.9 s | 6.1 s (only after the server closed it) |
new http.Agent({ keepAlive: true, timeout: 30000 }) | 3.9 s | 4.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:
| Wait | Result |
|---|---|
| 1,985 to 1,997 ms | reused (13 of 13) |
| 1,998 to 2,000 ms | ECONNRESET (3 of 3) |
| 2,001 to 2,015 ms | new 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.
- Node.js clients: give custom agents a
timeout, for examplenew http.Agent({ keepAlive: true, timeout: 30000 }). Measured above: it then follows the server's hint. - Node.js behind a load balancer or proxy: the proxy is the client. Set
server.keepAliveTimeoutlonger than the proxy's idle timeout. From the docs, not measured: the AWS Application Load Balancer idle timeout defaults to 60 s, so a common setting isserver.keepAliveTimeout = 65000. Older advice is to setheadersTimeoutabovekeepAliveTimeouttoo. - From the docs, not measured: nginx
keepalive_timeoutdefaults to 75 s for clients. For upstream keep-alive, nginx needskeepalivein theupstreamblock,proxy_http_version 1.1;andproxy_set_header Connection "";, and itskeepalive_timeoutthere (default 60 s) should be shorter than your app's. - Retry idempotent requests (GET, PUT, DELETE) once on
ECONNRESET.
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:
stream.respond({ ':status': 200, connection: 'keep-alive' })threwERR_HTTP2_INVALID_CONNECTION_HEADERS:HTTP/1 Connection specific headers are forbidden: "connection". The same happened withkeep-aliveand on the client side withsession.request().- The compatibility API
res.setHeader('Connection', 'keep-alive')did not throw. It printedUnsupportedWarning: The provided connection header is not valid, the value will be dropped, and the response had no such header. curl --http2-prior-knowledge -H "Connection: keep-alive"still shows the header in the-voutput, but it is not in the HTTP/2 frames curl logs, and the server did not receive it.
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
- Check the response:
curl -sI http://host/showsConnectionandKeep-Alive.curl -v url1 url2shows whether the second request saidRe-using existing. - In Python use a
requests.Session(). In Node.js use the global agent,fetch, or an agent withkeepAlive: trueand atimeout. - Make every client idle timeout shorter than the server's: browser and app to load balancer, load balancer to app.
- Leave the Node.js
keepAliveTimeoutabove 1000 ms so the header is nottimeout=0, and never 0 on a public server. - Do not set
ConnectionorKeep-Aliveon 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.