HTTP Keep-Alive vs TCP Keep-Alive: They Are Not the Same Header

HTTP keep-alive reuses connections for speed. TCP keep-alive detects dead peers. Mixing them up causes timeouts, leaked sockets, and load balancer failures.

Share
HTTP Keep-Alive vs TCP Keep-Alive: They Are Not the Same Header. Feature image on debugly.dev

HTTP keep-alive and TCP keep-alive solve different problems at different layers. HTTP keep-alive reuses a TCP connection across multiple requests to save handshake latency. TCP keep-alive sends idle probes from the kernel to detect dead peers. One is a performance optimisation; the other is a failure detector. Confusing the two is how load balancers drop connections they should keep.

According to RFC 7230, Section 6.3, persistent connections are the default in HTTP/1.1. The Linux kernel documents its TCP keep-alive parameters in the tcp(7) man page, where the defaults are 7200 seconds, 75 seconds, and 9 probes.

What each mechanism actually does

Property HTTP Keep-Alive TCP Keep-Alive
Layer Application (HTTP/1.1 default on) Transport (OS kernel, off by default on Linux)
Purpose Reuse a TCP connection for multiple HTTP requests Detect a dead or unreachable peer on an idle connection
How it works Server holds the connection open after a response; client sends the next request on the same socket OS sends a probe ACK after tcp_keepalive_time seconds of inactivity; if no ACK returns after tcp_keepalive_probes attempts, the connection is reset
Default on Linux Enabled in HTTP/1.1 (Connection: keep-alive is implicit) Off. tcp_keepalive_time = 7200s, tcp_keepalive_intvl = 75s, tcp_keepalive_probes = 9
What it costs Server memory per open connection; file descriptor pressure A few bytes per probe packet; negligible
What it saves TCP handshake RTT per request (typically 1-3 RTTs) Nothing. It only detects failure

The table is the single most important thing on this page. If you remember one thing, remember this: HTTP keep-alive is a performance optimisation, and TCP keep-alive is a failure detector. They operate at different layers, they solve different problems, and setting one does not configure the other.

How HTTP keep-alive works

In HTTP/1.1, persistent connections are the default. After the server sends a response, it does not close the TCP connection. The client sends the next request on the same socket, saving the TCP three-way handshake and any TLS negotiation. The Connection: keep-alive header is implicit in HTTP/1.1, and Connection: close is the opt-out.

On a typical Shopify app proxy call, a new TCP connection plus TLS 1.3 handshake adds roughly 50-80ms on a US-to-US connection. On a page that makes eight API calls, that is 400-640ms of handshake overhead that persistent connections eliminate.

The catch is server-side resource pressure. Every idle keep-alive connection holds a file descriptor and a buffer. The tuning question is the timeout: too short and you lose the reuse benefit, too long and you exhaust file descriptors.

How TCP keep-alive works

TCP keep-alive is an operating system feature, not an HTTP feature. When a TCP connection has been idle for tcp_keepalive_time seconds (default 7200 on Linux), the kernel sends a probe ACK. If the peer does not respond, the kernel sends another probe every tcp_keepalive_intvl seconds (default 75). After tcp_keepalive_probes failures (default 9), the kernel resets the connection. The total detection time at defaults is 7200 + (75 x 9) = 7875 seconds, which is over two hours.

The reason TCP keep-alive matters is that a half-open connection, where the peer has crashed or the network path has failed, does not trigger a TCP RST. Without keep-alive probes, the connection sits in ESTAB state forever, holding a file descriptor and a buffer. The application that owns the connection does not know the peer is gone until it tries to send data and the send times out, which could be minutes or hours depending on the TCP retransmission settings.

In production, the defaults are almost always wrong. Two hours is too long for a connection pool behind a load balancer. Most teams I have worked with set tcp_keepalive_time to 60 or 120 seconds for connections that traverse a load balancer or a NAT gateway, because those middleboxes silently drop idle connections after 5-10 minutes without sending a TCP RST.

Where teams get this wrong

The most common failure I see is setting an HTTP keep-alive timeout on the server and assuming it covers the TCP layer. It does not. The HTTP keep-alive timeout tells the server how long to wait for the next HTTP request before closing the connection. The TCP keep-alive timer tells the kernel how long to wait before probing a dead peer. They are independent, and the HTTP timeout is almost always shorter than the TCP default.

The second failure is assuming the load balancer's idle timeout and the application's keep-alive timeout are the same thing. They are not. If the load balancer's idle timeout is 60 seconds and the application's HTTP keep-alive timeout is 120 seconds, the load balancer will silently close the connection at 60 seconds, and the application will try to reuse a connection that no longer exists. The symptom is a periodic burst of 502 or 504 errors, and the cause is a timeout mismatch at the transport layer.

The third failure is enabling TCP keep-alive on a connection pool without adjusting the defaults. A connection pool that probes idle connections every two hours is a pool that holds dead connections for two hours. If the pool's max-idle timeout is shorter than the TCP keep-alive time, the keep-alive probes never fire, and the pool silently hands out dead connections.

The comparison that matters for your stack

Scenario What you need Why
High request rate, same server HTTP keep-alive with a short timeout (5-15s) Reuse connections, release file descriptors quickly
Connections through a NAT gateway or load balancer TCP keep-alive at 60-120s + HTTP keep-alive ≤ load balancer idle timeout Detect dead connections before the NAT table drops them
Long-lived connections (WebSocket, SSE) TCP keep-alive at 30-60s Detect dead peers without waiting for the next application message
Connection pool with max-idle timeout TCP keep-alive time < max-idle timeout Ensure probes fire before the pool hands out a dead connection
Debugging intermittent 502s Check both timeouts match across the proxy chain The cause is almost always a timeout mismatch

I hit the NAT gateway variant on a Shopify app making frequent Admin API calls. The cloud NAT gateway dropped idle connections after 5 minutes, and TCP keep-alive defaults were 2 hours. The symptom was a burst of connection reset errors every 5 minutes. The fix was setting tcp_keepalive_time to 60 seconds on every pooled socket.

How to check and set these on Linux

TCP keep-alive settings are per-socket, not per-application. The application has to opt in with setsockopt(SO_KEEPALIVE) and then set the three parameters. On Linux, you can check the system defaults:

cat /proc/sys/net/ipv4/tcp_keepalive_time     # 7200
cat /proc/sys/net/ipv4/tcp_keepalive_intvl    # 75
cat /proc/sys/net/ipv4/tcp_keepalive_probes   # 9

To change them globally (not recommended for production, but useful for testing):

sysctl -w net.ipv4.tcp_keepalive_time=120
sysctl -w net.ipv4.tcp_keepalive_intvl=30
sysctl -w net.ipv4.tcp_keepalive_probes=5

For production, set them per-socket in the application. In Go, net.TCPConn.SetKeepAlive(true) and SetKeepAlivePeriod(). In Node.js, socket.setKeepAlive(true, 120000). In Python, socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) plus the platform-specific options.

What I now do

Every connection pool in every service I touch gets the same review: HTTP keep-alive timeout set shorter than the load balancer idle timeout, TCP keep-alive set to 60-120 seconds, and the pool's max-idle timeout set shorter than the TCP keep-alive time. The three values have to be ordered correctly, and the ordering depends on the deployment topology. The defaults are wrong for every production environment I have seen.


Published on debugly.dev by Rohit Bhadani | Founder of Krova Cloud