TCP Keepalive Does Not Keep Your Connection Alive
You set up a service. It works fine for hours. Then you come back in the morning and SSH drops. The monitoring dashboard shows the connection is still “up.” You run ss -ti and see a socket stuck in ESTABLISHED state. The process on the other end died hours ago, but your server still thinks it’s connected.
This is the TCP keepalive trap.
The 2-Hour Default
TCP keepalive exists to detect dead peers. The idea is simple: after a period of silence, the kernel sends a probe packet. If the peer responds, the connection is alive. If not, the socket gets cleaned up.
The catch: Linux defaults to tcp_keepalive_time = 7200. That is two hours. Your application waits two hours before sending a single probe. Then it sends up to tcp_keepalive_probes = 9 probes, spaced tcp_keepalive_intvl = 75 seconds apart. Total detection time: 7200 + (9 * 75) = 7875 seconds. Over two hours and ten minutes.
A dead connection sits there consuming file descriptors, memory, and process slots for over two hours before your kernel notices something is wrong.
sysctl net.ipv4.tcp_keepalive_time
net.ipv4.tcp_keepalive_time = 7200
sysctl net.ipv4.tcp_keepalive_intvl
net.ipv4.tcp_keepalive_intvl = 75
sysctl net.ipv4.tcp_keepalive_probes
net.ipv4.tcp_keepalive_probes = 9
Enabling Keepalive Per-Socket
Enabling keepalive globally with sysctl affects every TCP connection on the system. Most of the time you want it on specific sockets, not all of them. Do it in application code:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Override the system defaults for this socket
if hasattr(socket, 'TCP_KEEPIDLE'):
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
This drops detection time from about 2 hours to 90 seconds (60 + 3 * 10). A dead peer gets flagged fast.
Why Application-Level Heartbeats Still Win
TCP keepalive probes are empty packets with no payload. They confirm the peer’s kernel is responding, not that the application is functioning. A server can be stuck in a deadlock, waiting on a database lock, or starved of worker threads and still respond to TCP keepalive probes because the kernel network stack handles them independent of the application.
This is why protocols like MQTT, gRPC, and PostgreSQL implement their own heartbeat or ping mechanisms at the application layer. A TCP keepalive probe reaching a dead application looks identical to one reaching a healthy one.
TCP keepalive confirms the kernel is alive. Application-level heartbeats confirm the service is alive. These are not the same thing.
When It Still Matters
Despite that limitation, TCP keepalive solves a real problem: cleaning up sockets left behind by crashed processes or network partitions. Without it, orphaned connections linger until the OS reclaims resources on reboot, or the remote side notices and sends RST. In environments with many long-lived connections, load balancers, database connection pools, message brokers, the accumulated cost of zombie sockets adds up.
The right approach is both: set reasonable TCP keepalive values for socket hygiene, and implement application-level heartbeats for service health detection.
# Quick system-wide tuning (not per-socket)
# Detect dead peers in 60 seconds, not 2+ hours
sysctl -w net.ipv4.tcp_keepalive_time=60
sysctl -w net.ipv4.tcp_keepalive_intvl=10
sysctl -w net.ipv4.tcp_keepalive_probes=3
Check what your application actually does with keepalive before tuning these. Some connection pools disable it entirely because they maintain their own health checks. Tuning the system default for those applications is wasted work, since they never enable the socket option in the first place.