April 4, 2026
Every two years I have to look this up again so I'm just going to write it down. The three knobs that matter on Linux are tcp_keepalive_time, tcp_keepalive_intvl, and tcp_keepalive_probes.
Default Linux: 7200 / 75 / 9. So the kernel sends the first probe after 2 hours of silence, then 8 more at 75 second intervals, then gives up. Total time-to-death of an idle connection: ~2h 11m. For modern apps behind NAT and stateful firewalls, that's an eternity — most middleboxes drop idle flows after 5 to 15 minutes.
net.ipv4.tcp_keepalive_time = 120 net.ipv4.tcp_keepalive_intvl = 15 net.ipv4.tcp_keepalive_probes = 4
So: first probe after 2 minutes idle, 4 more at 15s, total ~3 minutes to detect a dead peer. Aggressive enough to keep NATs happy, not so aggressive that a transient 30s blip kills the connection.
Sysctls are the system-wide default. If a specific app needs different behavior, set it on the socket:
setsockopt(fd, SOL_TCP, TCP_KEEPIDLE, &120, sizeof(int)); setsockopt(fd, SOL_TCP, TCP_KEEPINTVL, &15, sizeof(int)); setsockopt(fd, SOL_TCP, TCP_KEEPCNT, &4, sizeof(int));
If your protocol has a ping frame (WebSocket, gRPC, HTTP/2) — use that. Application-level keepalives let you measure RTT and react in code, and they don't depend on the TCP stack agreeing with you about what "alive" means.
On macOS the sysctls are named differently. net.inet.tcp.keepidle takes milliseconds, not seconds. I have lost time to this more than once.