TCP Keep Alive and HTTP Keep Alive Are Not the Same Feature
I have watched two engineers argue for twenty minutes about whether keep alive was enabled. One was looking at a Connection: keep-alive header. The other was looking at net.ipv4.tcp_keepalive_time. They were both right and they were talking about different layers of the stack.
The naming collision is genuinely unfortunate, and it has sent me down the wrong path more than once. So here is the whole thing in one place.
The one sentence version
HTTP keep alive is connection reuse: do not close the socket after each response. TCP keep alive is dead peer detection: send a probe on an idle socket to find out whether the other end still exists.
Everything else follows from that distinction.
HTTP keep alive, properly
HTTP/1.0 closed the connection after every response. That meant a new TCP handshake and, with TLS, a new full handshake for every single asset on a page. HTTP/1.1 made persistence the default, which is what Connection: keep-alive actually signals, and the header is largely ceremonial today because the behaviour is assumed.
What it buys you is reuse. One TCP connection carries many request and response pairs in sequence. Your browser opens six of them per origin and pipelines work across them.
The important property: HTTP keep alive is about throughput and setup cost. It has nothing to do with detecting failure. If the far end vanishes, HTTP keep alive will not tell you. You will find out when your next write fails or your read times out.
HTTP/2 removed the header entirely and replaced the model. One connection, many concurrent streams. Same goal, different mechanism, and a new failure mode of its own that I covered in HTTP/2 head of line blocking.
TCP keep alive, properly
TCP keep alive is a kernel feature. When a connection has been idle for tcp_keepalive_time seconds, the kernel sends a probe segment with no data and an acknowledgement number one less than expected. A live peer responds with an ACK. A dead peer does not respond at all.
The defaults on Linux are the part that surprises everyone:
| Parameter | Default | Meaning |
|---|---|---|
tcp_keepalive_time |
7200 | Seconds idle before the first probe |
tcp_keepalive_intvl |
75 | Seconds between probes |
tcp_keepalive_probes |
9 | Failed probes before the connection is dropped |
That is two hours of silence before anything happens, then eleven minutes of probing, then the connection dies. Roughly two hours and eleven minutes to notice that a peer disappeared.
Check yours:
sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes
On a per socket basis, most runtimes let you override. In Node:
const socket = net.connect({ host, port });
socket.setKeepAlive(true, 30_000); // enable, first probe after 30s
Node also exposes server.keepAliveTimeout and server.headersTimeout, which are HTTP layer settings with confusingly similar names. keepAliveTimeout is how long the server holds an idle reusable connection open before closing it. That is HTTP keep alive, not TCP keep alive, and it is the setting that causes the ECONNRESET pattern in connection reset by peer at exactly sixty seconds.
Why the confusion costs you
Three debugging traps come directly from mixing these up.
Tuning the wrong knob. You have a connection pool holding sockets that go stale behind a NAT gateway with a five minute idle timeout. You lower tcp_keepalive_time to 60. Nothing improves, because the NAT is dropping the flow based on its own idle timer and your probes are correctly keeping the kernel's view alive but arriving too late. The fix is usually an application level ping or a shorter pool idle timeout, not a kernel parameter.
Assuming keep alive means health. A load balancer marks a backend healthy because its TCP connection is alive. TCP keep alive tells you the kernel on the far end still responds to ACKs. It says nothing about whether the application thread is deadlocked, whether the event loop is blocked, or whether the process is technically running but unable to serve. This is the same gap that outage caused by a successful health check is about.
Looking for a header that does not exist. In HTTP/2 and HTTP/3 there is no Connection header at all. If you are grepping for keep alive in a capture of an h2 session you will find nothing and conclude incorrectly that reuse is off.
Checking what is actually enabled on a live socket
The kernel settings tell you the system default, not what a given connection is doing. To see the per socket state:
ss -tino state established '( dport = :443 )' | head
The -o flag prints timer information. A socket with TCP keep alive enabled shows a keepalive timer with a countdown. A socket without it shows no timer at all, which means it will sit idle forever and never discover that the peer is gone.
On the application side, the question is whether your HTTP client reuses connections at all. Most do by default, but plenty of code creates a fresh client per request, which silently disables HTTP keep alive no matter what the headers say. In Node that means sharing one Agent with keepAlive: true; in Python it means holding one requests.Session or httpx.Client instead of calling the module level helpers; in Go it means reusing http.Client and not setting DisableKeepAlives.
If you are opening a new connection per request, your TLS handshake cost is paid every single time, and no kernel tuning will help.
Which one do you actually want
If your problem is handshake overhead, you want HTTP keep alive, or better, HTTP/2 multiplexing. Measure with the curl timing template in the curl flag that tells you which part was slow and look at the connect and TLS phases.
If your problem is detecting a peer that silently disappeared, you want TCP keep alive with a much shorter tcp_keepalive_time than the default, or an application level heartbeat. Two hours is never what you want in production.
If your problem is idle connections being killed by something in the middle, neither will save you on its own. You need your idle timeout to be shorter than every intermediate device's idle timeout, which means finding out what those are.
The rule I keep: TCP keep alive answers "is the socket still alive". HTTP keep alive answers "should I open a new socket". Those are different questions, and answering the wrong one is how you spend an afternoon editing sysctls.