How-to

Why your SSH session keeps dropping, and how to keep it alive

7 min read

You leave a session open, go to a meeting, come back and type — and the terminal sits there for a moment before printing:

client_loop: send disconnect: Broken pipe

Or Connection reset by peer, or Connection to example.com closed by remote host. All three describe the same experience and three different causes. Sorting out which one you have takes a minute and saves you from applying the wrong fix forever.

Why idle SSH sessions die

An SSH connection that is not transferring anything sends no packets at all. This is efficient and, on the modern internet, a liability: a lot of equipment between you and the server keeps a table of active connections and drops entries that go quiet.

  • NAT routers — home routers, office gateways, mobile tethering. Idle TCP timeouts of 5 to 30 minutes are typical, and on carrier-grade NAT they can be under 5.
  • Stateful firewalls — corporate and cloud ones commonly cut idle TCP at 30 or 60 minutes. AWS Network Load Balancer, for one, has a fixed 350-second idle timeout that you cannot change.
  • Captive portals and hotel Wi-Fi — frequently aggressive, occasionally hostile.

Nothing tells either end that the entry has been dropped. The connection is not closed, it is forgotten. Both sides still believe it exists, which is why you find out only when you type something and the packet goes nowhere.

The fix, on your Mac

Make the client send something periodically. In ~/.ssh/config:

Host *
    ServerAliveInterval 30
    ServerAliveCountMax 6

ServerAliveInterval 30 sends a small encrypted request every 30 seconds of silence, which keeps the NAT entry warm. ServerAliveCountMax 6 means SSH gives up after six unanswered ones — so a genuinely dead connection is declared dead after about three minutes, instead of hanging forever.

Remember the first-match-wins rule: this block belongs at the bottom of the file, or a more specific Host block above it will not inherit it.

To try it for one connection before committing:

ssh -o ServerAliveInterval=30 prod

The one that looks similar and is not: TCPKeepAlive

TCPKeepAlive yes is a different mechanism and a weaker one. It uses TCP-level keepalives, which are sent unencrypted and unauthenticated, and — the part that matters — a lot of middleboxes do not count them as activity, so they fail to hold the NAT entry open.

ServerAliveInterval sends real SSH protocol messages inside the encrypted channel. It is the one that works. Leave TCPKeepAlive at its default and do not treat it as a substitute.

When the server is the one hanging up

If the drop happens at a suspiciously consistent interval — always at 5 minutes, always at 10 — the server is enforcing it, and client keepalives will not help because the server is not counting them as activity.

In /etc/ssh/sshd_config:

ClientAliveInterval 60
ClientAliveCountMax 5

Confusingly, these are the server’s version of the same idea: they keep the connection warm from the other direction. But a short ClientAliveInterval with a small ClientAliveCountMax is also how administrators deliberately reap idle sessions — ClientAliveInterval 300 with ClientAliveCountMax 0 means “disconnect after five minutes of silence”, full stop.

Some hardened servers also set IdleTimeout or use a shell-level TMOUT environment variable, which logs you out from the shell rather than the SSH layer. If your session ends with timed out waiting for input: auto-logout, that is TMOUT, and no SSH setting will change it — it is the login shell doing it. echo $TMOUT on the server confirms.

After editing sshd_config, check the syntax before restarting, or you can lock yourself out:

sudo sshd -t && sudo systemctl reload sshd

Reading the error you actually got

client_loop: send disconnect: Broken pipe — you tried to send on a connection whose path no longer exists. The classic idle-timeout signature. ServerAliveInterval is the fix.

Connection reset by peer — something sent a TCP RST. A firewall actively killing the connection, or the server process dying. Less likely to be a plain idle timeout.

Connection to X closed by remote host — the server closed it deliberately: ClientAlive reaping, TMOUT, a reboot, or sshd being restarted.

packet_write_wait: Connection to X port 22: Broken pipe — same as the first, usually caught mid-output.

Session freezes but never errors — the connection is gone and SSH has not noticed because no keepalives are configured. Escape it with Enter, then ~. (tilde, period) — SSH’s escape sequence to force a local disconnect without waiting.

Wi-Fi changes, sleep, and why your Mac is different

Two things specific to laptops:

Changing network — walking from Wi-Fi to Ethernet, or between access points on different subnets, changes your IP. The TCP connection is bound to the old one and cannot survive. No keepalive setting helps; the connection is genuinely gone.

Sleep — when the lid closes, the Mac stops servicing the connection. Anything past a short nap comes back to a dead session. Keeping the connection nominally alive across sleep is not something SSH can do.

If either of these is your daily reality, keepalives are the wrong tool and you want one of these instead:

  • tmux or screen on the server. The session lives on the server, so your connection dying is an inconvenience rather than lost work. tmux new -A -s main attaches or creates in one command. This is the answer for long-running jobs, and the only one that survives a laptop being closed for a day.
  • mosh, which is built for roaming and sleep, at the cost of installing it on every server and giving up SSH’s exact terminal semantics.
  • Automatic reconnection in the client — reopening the connection for you, which handles the network-changed case cleanly but cannot restore the shell’s state. Anything running in the foreground is still gone.

Automatic reconnection, and when it should not fire

Reconnecting on a dropped network is straightforwardly useful. Reconnecting on anything is not.

Upshell reconnects when the transport fails — network changed, Wi-Fi dropped, laptop woke up — and deliberately does not reconnect after a rejected password or a changed host key. Both of those are cases where retrying makes things worse: repeated attempts trip fail2ban and lock you out of your own server, and retrying through a host key mismatch means handing credentials to whatever is answering on that address. That distinction is the whole difference between a helpful feature and a dangerous one.

Port forwards are re-established with the session, which matters if you are tunnelling a database connection and would rather not rebuild it by hand every time you move desk.

Step by step

  1. Time the drops. Consistent interval means the server is reaping; random means an idle timeout in the middle. This determines which end you fix.
  2. Add client keepalivesServerAliveInterval 30 and ServerAliveCountMax 6 in a Host * block at the bottom of ~/.ssh/config.
  3. Test with ssh -o ServerAliveInterval=30 <host> before editing the file, if you want to confirm first.
  4. If it still drops on a fixed schedule, check the serverClientAliveInterval and ClientAliveCountMax in sshd_config, and echo $TMOUT for a shell-level logout.
  5. If you close the laptop or change network often, run tmux on the server. Keepalives cannot save a connection whose IP no longer exists; tmux makes that stop mattering.

Questions people ask

What is a good ServerAliveInterval value?

30 to 60 seconds suits almost everyone. Shorter than 15 is pointless traffic; longer than 120 risks being above the idle timeout you are trying to beat, especially on mobile networks.

ServerAliveInterval or TCPKeepAlive?

ServerAliveInterval. It sends encrypted SSH-level messages that middleboxes reliably count as activity. TCP keepalives are sent below the encryption and are frequently ignored by the very equipment causing the problem.

Why does my session die exactly when I stop typing for a while?

Something between you and the server has an idle timeout — usually a NAT router or a stateful firewall — and dropped the connection from its table. Client keepalives prevent it by making sure the connection is never actually idle.

Does SSH survive closing my MacBook lid?

No. The connection is bound to a TCP socket the machine stops servicing during sleep, and it is usually dead on wake. Run tmux on the server so the work survives even though the connection does not.

Upshell — the native SSH & SFTP client for macOS

Tabbed and split terminal, saved hosts with keys and 2FA, multi-hop bastions, port forwarding and SFTP on the very same connection. Free to download, no subscription required.