·8 min read

SSH Keepalives: Stop Dropped Connections and Stay Connected

SSH sessions silently dying after idle time? 'Broken pipe' and 'connection reset' are usually NAT timeouts, not your server. Here's how keepalives work and how to keep connections alive — on the client, on the server, and inside FoxTerm.

SSHNetworkingKeepaliveStabilityDevOpsTroubleshooting

You're mid-deployment, watching log output stream by. You step away for coffee. When you come back, the terminal is frozen. You press a key — nothing. A moment later: Write failed: Broken pipe. Your session is gone, and whatever command was running died with it.

If this sounds familiar, your server isn't necessarily at fault. More often than not, the connection was killed by a middlebox — a NAT gateway, firewall, or router that silently dropped an idle connection. The fix is keepalives: small, periodic messages that tell every device along the path "this connection is still alive."

This guide covers why SSH sessions die, how the two keepalive mechanisms actually work, and the exact settings to keep your connections stable — on the client, on the server, and inside FoxTerm.

Why SSH Sessions Die

Your Mac ──(SSH)──> NAT / Firewall ──(SSH)──> Remote Server

Every network path between you and your server has state. NAT gateways and stateful firewalls track each connection in memory — source IP, destination, ports, and last activity. That state costs memory, so devices reclaim it when a connection goes quiet. When that happens, the firewall "forgets" your connection and starts dropping its packets. Your SSH client doesn't know until it tries to send something and gets nothing back.

The usual suspects:

  • NAT idle timeouts — cloud NAT gateways and home routers drop connections with no traffic after a few minutes to a few hours
  • Stateful firewalls — corporate and cloud security groups do the same, sometimes aggressively
  • IP changes — DHCP renewals on mobile and hotspot networks quietly break the old path
  • Server-side limits — sshd can be configured to disconnect idle or unresponsive clients
  • Network path changes — Wi-Fi roaming, VPN reconnects, and carrier handoffs all reset connection state

SSH is interactive: when you stop typing, no packets flow at all. To a firewall, a session you're not using looks exactly like a dead one. That's why connections always die during idle stretches — never while you're actively typing.

Keepalive Basics: Two Mechanisms, One Goal

There are two different keepalive mechanisms in the SSH stack, and confusing them is the most common mistake. One works well; the other mostly doesn't.

TCPKeepAlive: The OS-Level Probe

TCP keepalive is built into the operating system, not SSH. Every so often the kernel sends an empty TCP probe on the connection, with no payload. OpenSSH enables this by default (TCPKeepAlive yes).

The problem: these probes are not SSH traffic. They're bare TCP ACKs, and many firewalls and NAT devices drop empty packets or don't refresh connection state for them. TCP keepalive is also tuned for detecting dead peers, not keeping NAT mappings warm — on Linux the first probe typically doesn't fire until two hours of idle time (tcp_keepalive_time = 7200). For keeping SSH sessions alive through middleboxes, it's mostly useless.

ServerAliveInterval: The SSH-Level Heartbeat

This is the mechanism that actually works. The SSH client sends a real, encrypted SSH protocol message (a no-op channel request) at a fixed interval. The server responds, the messages count as genuine traffic, and every NAT device and firewall along the path sees an active connection.

If the server stops responding, the client sends ServerAliveCountMax probes and then gives up, surfacing a clear error instead of hanging silently.

ssh -o ServerAliveInterval=60 user@server

Client-Side Configuration

The recommended fix for dropped sessions is ServerAliveInterval, set in ~/.ssh/config.

Per Host

# ~/.ssh/config
Host production
    HostName prod.example.com
    User deploy
    ServerAliveInterval 60
    ServerAliveCountMax 3

ServerAliveInterval 60 sends a keepalive every 60 seconds. ServerAliveCountMax 3 means the client tries three probes before declaring the connection dead — roughly three minutes of unresponsiveness.

Global Default

If you want every connection protected:

Host *
    ServerAliveInterval 60

One-Off Without Editing Config

ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=3 user@server

Don't Forget Port Forwards

If you keep ssh -L or ssh -D tunnels alive, a disconnected tunnel silently stops forwarding. Make failures explicit:

Host tunnel
    HostName bastion.example.com
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ExitOnForwardFailure yes

ExitOnForwardFailure yes kills the session if a port forward can't be established — no more "the tunnel is up but nothing routes through it."

Server-Side Configuration

Sometimes the server is the one ending sessions. Two sshd_config directives control this:

# /etc/ssh/sshd_config
ClientAliveInterval 60
ClientAliveCountMax 3

With these set, the server sends keepalive messages to idle clients. This serves two purposes: it keeps connections alive through firewalls on the client's side, and it lets the server detect and disconnect clients that have truly vanished — preventing zombie sessions that accumulate on busy hosts.

sudo systemctl reload ssh    # or: sudo systemctl restart sshd

A common server-side mistake is setting ClientAliveInterval very low to "clean up" idle sessions. Remember that any value below a few minutes will also kill legitimate idle sessions — the users who keep a session open for an hour and expect it to still be there.

Choosing the Right Interval

There's no universal number; the right interval depends on the most aggressive idle timeout on your path. As a rule of thumb: keep your interval well below half of the smallest timeout you know of.

Scenario Recommended ServerAliveInterval Why
Cloud NAT (AWS, Azure, GCP) 30–60s Cloud gateways drop idle TCP aggressively
Home / office router NAT 60s Consumer routers commonly time out in 5–30 minutes
Corporate firewall 60–120s Long timeouts, fewer probes needed
Mobile / hotspot / roaming 15–30s Carrier and device timeouts are the most aggressive
Stable LAN, direct connection 120s+ or disabled No middlebox to keep alive

A 60-second default covers most real-world paths. If you're on a mobile connection or a strict corporate network, start at 30 seconds and test. If you know your NAT timeout is, say, 10 minutes, an interval of 60–120 seconds leaves a comfortable margin.

Surviving Disconnects (Not Just Preventing Them)

Keepalives prevent silent drops, but sometimes a connection dies anyway — a network change, a server reboot, a power outage. When that happens, you don't want to lose your work. Two tools make disconnects survivable:

tmux / screen: Sessions That Outlive the Connection

Run long operations inside tmux on the server, and the session keeps running even if your SSH link drops. When you reconnect, you simply reattach:

tmux new -s deploy        # start a named session
# ... work, disconnect, come back ...
ssh server
tmux attach -t deploy     # everything is still there

tmux is the single highest-leverage habit for anyone who runs long tasks over SSH.

mosh: Roaming Without Drops

mosh uses its own UDP-based protocol instead of SSH's TCP, with per-keystroke synchronization and connection roaming. When your IP changes — Wi-Fi to LTE, laptop to dock — mosh keeps the session alive across the change. It requires UDP access, so it doesn't work through every firewall, but where it works, it makes drops nearly invisible.

Troubleshooting Common Failures

Error Likely cause Fix
Write failed: Broken pipe Socket closed while writing — usually a middlebox or server timeout Set ServerAliveInterval; run work inside tmux
Connection reset by peer Firewall or server killed the connection Check ClientAliveInterval on the server; verify firewall rules
Timeout, server not responding The client gave up after unanswered keepalives Increase ServerAliveCountMax; check network path
Session hangs, no error at all Middlebox dropped it; client never noticed Add ServerAliveInterval so the client detects and reports it
ssh -L tunnel silently stops working Forwarded connection died without your client noticing Add ServerAliveInterval + ExitOnForwardFailure yes

When diagnosing, verbose mode shows exactly what's happening:

ssh -vvv user@server

On the server, check the current settings:

sshd -T | grep -i clientalive

And if you suspect a firewall on the path, a quick capture tells you whether keepalives are actually flowing:

tcpdump -i en0 port 22

FoxTerm: Keepalives Without the Config Files

All of this works beautifully with raw ~/.ssh/config — but that's exactly the kind of per-host bookkeeping FoxTerm was built to eliminate. In FoxTerm, connection stability is handled in the UI:

  • Per-connection keepalive settings — set the interval and retry count per server in the connection editor, no config files to manage
  • Auto-reconnect with backoff — when a connection drops, FoxTerm reconnects automatically, with exponential backoff to avoid hammering the server; if your session ran inside tmux, one click reattaches you to it
  • Connection health in the monitoring sidebar — the same sidebar that tracks CPU, memory, and network now shows link state and keepalive activity, so you see a disconnect coming before it costs you a session
  • One-click reconnect for groups — organize servers by environment or region, and reconnect a whole group with a single click
  • Zero telemetry — keepalive traffic runs entirely inside the SSH connections you initiate; nothing else leaves your Mac

The result: the configuration patterns in this guide are already handled for you, with sane defaults — and you can always drop back to custom values when a specific network demands it.

Conclusion

Dropped SSH sessions are almost never your server's fault. They're the result of idle connections being reclaimed by NAT gateways and firewalls — and the fix is simple and well-understood:

  1. Client side: set ServerAliveInterval 60 (and ServerAliveCountMax 3) in ~/.ssh/config
  2. Server side: consider ClientAliveInterval for zombie-session cleanup, but don't set it too low
  3. Sessions: run long work in tmux so a drop never costs you progress
  4. Tools: let FoxTerm handle keepalives and auto-reconnect per connection, so this never becomes a manual chore again

Keepalives are boring. But the five minutes it takes to set them up is the difference between a workflow that survives network reality and one that resets every time the network sneezes.


Download FoxTerm at foxterm.app.