client_loop: send disconnect: Broken pipe means your SSH client tried to send data over a connection that no longer existed. When it happens after a session has sat idle, something between you and the server dropped the idle connection: usually a home router’s NAT table, an office firewall, or a laptop that went to sleep. Your next keystroke then hits a dead TCP connection. The fix is a keepalive, so the connection never looks idle, plus tmux for long jobs, so a drop costs you nothing.
Host *
ServerAliveInterval 60
ServerAliveCountMax 3That makes the client send a small encrypted “still there?” message after every 60 seconds of silence. You can set the same thing up on the server instead, and run anything that takes a while inside tmux:
ClientAliveInterval 60
ClientAliveCountMax 3After saving that file, run sudo sshd -t && sudo systemctl reload ssh (the service is called sshd on Rocky, Alma and CentOS Stream).
tmux new -s work # start a named session and run the job inside it
# Ctrl+b, then d: detach on purpose, the job keeps running
tmux attach -t work # after you reconnectWhat does “Broken pipe” mean in SSH?
“Broken pipe” is the operating system’s error for writing to a connection whose other end is gone. It is not an authentication problem or a broken config file. The SSH client had something to send, such as a keystroke, a window resize or its own disconnect message, and the TCP connection under it was already dead. Older OpenSSH clients report the same situation as Write failed: Broken pipe or packet_write_wait: Connection to 203.0.113.10 port 22: Broken pipe.
When it happens tells you more than the message does:
- Only after the session sat idle. A router or firewall forgot the connection while nothing was sent. Keepalives fix this, as described below.
- After the laptop slept or changed networks. The connection died with the old network. Keepalives cannot help; tmux or mosh can. See sleep and network changes.
- In the middle of big output or a transfer. Idle timeouts are not the cause. Look at MTU, memory and reboots, covered in drops under load.
- For everyone logged in at the same moment. Then it is the server or its network, not your router.
Two related messages look similar but mean something else. Connection to 203.0.113.10 closed by remote host means the server closed the session on purpose, for example during a reboot. Timeout, server 203.0.113.10 not responding is the keepalive from this article doing its job: three probes went unanswered, so the client gave up instead of hanging forever. If the connection is refused or reset before you even log in, that is a different problem: see Connection reset by peer and Connection timed out.
How do I fix it on the client?
Linux and macOS
Put the keepalive in ~/.ssh/config on the computer you connect from. Create the file if it does not exist:
# specific hosts first: ssh uses the first value it finds for each option
Host myvps
HostName 203.0.113.10
User deploy
# defaults for every host
Host *
ServerAliveInterval 60
ServerAliveCountMax 3chmod 700 ~/.ssh
chmod 600 ~/.ssh/configServerAliveInterval 60 sends a probe through the encrypted connection after 60 seconds without traffic. That keeps the entry in your router’s NAT table fresh. ServerAliveCountMax 3 means that after three unanswered probes, about three minutes, the client disconnects cleanly with “Timeout, server not responding” instead of freezing. If some networks still drop you, mobile hotspots and carrier-grade NAT among them, lower the interval to 30 or 15.
To try it once without editing anything:
ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=3 deploy@203.0.113.10Windows: OpenSSH and PuTTY
The OpenSSH client built into Windows 10, Windows 11 and current Windows Server versions reads %USERPROFILE%\.ssh\config, with the same lines as above. In PowerShell:
New-Item -ItemType Directory -Force "$HOME\.ssh" | Out-Null
Add-Content -Path "$HOME\.ssh\config" -Value "Host *", " ServerAliveInterval 60", " ServerAliveCountMax 3"Add-Content appends to the end of the file and creates it if it is missing, so an existing config is kept. In PuTTY, open the saved session, go to Connection, set Seconds between keepalives to 60, then go back to Session and click Save. Otherwise the setting is lost with the window.
How do I fix it on the server?
Set it on the server when you cannot change every client, for example on a team server, or when you also want the server to clean up sessions whose client has vanished. Debian 11 and newer, Ubuntu 20.04 and newer and Rocky Linux 9 all read /etc/ssh/sshd_config.d/*.conf through an Include line at the top of sshd_config, so a small file there is enough:
printf 'ClientAliveInterval 60\nClientAliveCountMax 3\n' | sudo tee /etc/ssh/sshd_config.d/10-keepalive.conf
sudo sshd -t # no output means the config is valid
sudo systemctl reload ssh # Debian, Ubuntu
sudo systemctl reload sshd # Rocky, AlmaLinux, CentOS Stream
sudo sshd -T | grep -i clientalive # the values sshd actually usessshd keeps the first value it reads for each option, and the included files are read in name order before the rest of sshd_config. The 10- prefix makes your file win over a later setting in another drop-in. A reload applies to new connections only, so log out and back in to test. When a client stops answering, the server now ends the session after three missed probes and logs Timeout, client not responding, instead of keeping a dead session open.
ServerAlive or TCPKeepAlive: what’s the difference?
Both sides also have a TCPKeepAlive option, and it is already on by default. It rarely prevents this error:
- TCPKeepAlive uses the kernel’s TCP keepalives. On Linux the first one is sent only after two hours of silence (
net.ipv4.tcp_keepalive_time = 7200), long after many routers have forgotten the connection. The probes travel outside the encryption, and the SSH manual pages point out that they can be spoofed. - ServerAliveInterval (client) and ClientAliveInterval (server) are SSH messages inside the encrypted channel. You set the interval in seconds, and they detect a dead peer as well as keeping the path busy.
Leave TCPKeepAlive on and use the SSH-level options to fix the drops.
What if the laptop sleeps or switches networks?
No keepalive helps here. A sleeping laptop sends nothing, so the server or a router times the connection out. Moving from Wi-Fi to a mobile hotspot changes your public IP address, and a TCP connection is tied to the address it started from. The connection is gone either way, so make the drop harmless instead:
- Run your work in tmux (or screen). Reconnect, run
tmux attach -t work, and you are back where you left off. - Use mosh for an interactive shell that survives sleep and roaming. It logs in over SSH, then switches to UDP and follows you across IP changes. It needs the package on both ends and UDP ports 60000–61000 open on the server. It offers no port forwarding and little scrollback, which is why it pairs well with tmux.
sudo apt install -y mosh # on the server and on your computer
sudo ufw allow 60000:61000/udp # on the server, if it runs ufw
mosh deploy@203.0.113.10 -- tmux new -A -s work # log in and attach (or create) the sessiontmux new -A -s work attaches to the session called work if it exists and creates it if not, so the same command works every time. On Windows, mosh runs inside WSL.
What if it drops during big output or transfers, not when idle?
If the session survives idle time but breaks while a lot of data is moving (a long log scrolling past, journalctl without a pager, an scp), keepalives are not the issue. Check these three:
- MTU on a VPN or tunnel. Small packets such as keystrokes pass, but full-size packets are dropped somewhere along the path, and the “packet too big” message that should report it is filtered. The session freezes on the first large burst and then breaks. Test it with
ping -M do -s 1472 203.0.113.10on Linux (macOS:ping -D -s 1472, Windows:ping -f -l 1472), and lower the size until it gets through. If only smaller sizes pass, reduce the MTU of the VPN interface, for exampleMTU = 1380in a WireGuard[Interface]section. - The server ran out of memory. The kernel’s OOM killer may have killed your shell, its sshd session process or the job you were running. Check with
sudo journalctl -k | grep -i "killed process", and see why Linux kills processes when memory runs out. - The server rebooted.
uptime -sprints the time of the last boot, andjournalctl --list-bootslists earlier ones. If those times match your drops, read VPS keeps rebooting.
How do I keep a job running after I disconnect?
A command started in a plain SSH shell gets a hangup signal when the connection dies, and most programs exit on it. If an apt upgrade is killed that way, dpkg is left half-finished (see dpkg was interrupted). Pick one of these, depending on the job:
sudo apt install -y tmux # Rocky, Alma: sudo dnf install -y tmux
tmux new -s work # new session
tmux ls # list sessions after reconnecting
tmux attach -t work # back in
# inside tmux: Ctrl+b d detaches, Ctrl+b [ scrolls (q to leave)nohup ./import.sh > import.log 2>&1 &
tail -f import.log # follow it, also from a new sessionsudo systemd-run --unit=reindex /usr/local/bin/reindex.sh
journalctl -u reindex -f # its output
systemctl status reindex # still running?- tmux or screen when you want to watch it, answer prompts or come back to the same terminal. This is the right choice for upgrades.
- nohup for a fire-and-forget command. You cannot reattach, but the log file has everything.
- A systemd service for anything that should run permanently, such as a bot, an app or a worker, and restart after a crash or reboot. See how to create a systemd service.
Where is the connection being lost?
If keepalives do not help and the drops follow no pattern, find out where packets get lost. Run mtr from your computer to the server, and from the server back to you. Inside an SSH session, echo $SSH_CLIENT prints your public IP as the first field:
# on your computer
mtr -rwbzc 100 203.0.113.10
# on the server, towards the IP that $SSH_CLIENT shows
mtr -rwbzc 100 198.51.100.7
# the server's view of the drops (unit: ssh on Debian/Ubuntu, sshd on Rocky/Alma)
sudo journalctl -u ssh --since "2 hours ago" | grep -iE "timeout|reset|closed"Loss that starts at one hop and continues to the last line is real. Loss at a single hop in the middle usually is not. Our guide to reading an mtr report explains the columns.
In practice the drop is almost always on the path to the server, not on the server itself: a VPS keeps an SSH session open for as long as packets reach it. When SSH will not stay up long enough to investigate, every VMHeaven KVM VPS has a VNC console in the panel, which does not depend on SSH or on your router.
Frequently asked
What is the difference between ServerAliveInterval and ClientAliveInterval?
ServerAliveInterval is a client option in ~/.ssh/config: your ssh client sends a probe to the server after that many seconds of silence. ClientAliveInterval is the same idea on the server side in sshd_config: sshd probes the client. Both travel inside the encrypted connection, keep routers from forgetting an idle session and end the connection cleanly after the matching CountMax probes go unanswered. Setting either one fixes idle drops; the client option is the one you can change without root on the server.
Will keepalives keep me logged in forever?
They keep an idle connection open, but they don't override anything that closes it on purpose. A TMOUT variable in the server's shell profile logs idle shells out (check with 'echo $TMOUT'), some firewalls cap how long a session may last, and a reboot, a sleeping laptop or a network change still ends the connection. For work that must survive all of that, run it in tmux on the server.
How do I get back to my session after a drop?
Only if it ran inside tmux or screen. Reconnect with ssh, run 'tmux ls' to list sessions and 'tmux attach -t work' to return to yours, with the job still running. A command started in a plain SSH shell usually received a hangup signal and exited when the connection died. Check with 'pgrep -af <name>' whether it is still running, and next time start it in tmux, with nohup or as a systemd service.
Why do I get Broken pipe during scp?
A transfer that breaks while data is flowing is rarely an idle timeout. Common causes are an MTU problem on a VPN, where full-size packets are silently dropped, an unstable Wi-Fi or mobile link, or the server running out of memory. Add '-o ServerAliveInterval=30' and switch to rsync with '--partial --progress' so an interrupted copy resumes instead of starting over.