> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# HTTP Keep-Alive vs TCP Keep-Alive: They Are Not the Same Header
- URL: https://debugly.dev/http-keep-alive-vs-tcp-keep-alive-comparison/
- Published: 2026-10-16T13:30:00.000Z
- Updated: 2026-10-17T05:47:50.000Z
- Description: HTTP keep-alive reuses connections for speed. TCP keep-alive detects dead peers. Mixing them up causes timeouts, leaked sockets, and load balancer failures.
- Author: Rohit Bhadani
- Tags: DevOps

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](https://httpwg.org/specs/rfc7230.html?ref=debugly.dev#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](https://man7.org/linux/man-pages/man7/tcp.7.html?ref=debugly.dev), 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](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)