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.
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 —
sshdcan 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:
- Client side: set
ServerAliveInterval 60(andServerAliveCountMax 3) in~/.ssh/config - Server side: consider
ClientAliveIntervalfor zombie-session cleanup, but don't set it too low - Sessions: run long work in
tmuxso a drop never costs you progress - 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.