You’ve probably experienced this: you kick off a long docker build or a kubernetes rollout via SSH, go get coffee, and come back to a dead terminal. The session didn’t hang; it was silently killed by an intermediate NAT gateway or firewall that reaped the idle TCP connection. This is frustrating because the remote process might still be running, but you’ve lost your ability to monitor it.
The 5-minute setup
The fix is twofold: enable keepalives in your SSH client configuration to prevent the firewall from seeing the connection as idle, and use nohup or tmux to ensure the remote command survives the session disconnect.
First, update your ~/.ssh/config to send keepalive probes every 60 seconds. This prevents intermediate devices from dropping the connection due to inactivity. Add or modify these lines in your Host * block (or specific host blocks):
# ~/.ssh/config
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
TCPKeepAlive yes
ServerAliveInterval 60: Sends an encrypted null packet to the server every 60 seconds.ServerAliveCountMax 3: Allows up to 3 missed keepalives before the client gives up.TCPKeepAlive yes: Enables TCP-level keepalives (OS level), which are slower but provide a safety net.For immediate relief on an already-opened session, you can’t change the client config retroactively, but you can prevent the remote command from dying. Instead of running:
ssh user@prod-server "tar -czf /tmp/logs.tar.gz /var/log/app"
Use nohup to detach the process from the shell session:
ssh user@prod-server "nohup tar -czf /tmp/logs.tar.gz /var/log/app > /dev/null 2>&1 &"
However, the gold standard is using tmux or screen. If tmux is installed on the remote host:
# Start a detached tmux session and run the command
ssh user@prod-server "tmux new-session -d -s backup 'tar -czf /tmp/logs.tar.gz /var/log/app'"
# Later, reattach to monitor progress
ssh user@prod-server "tmux attach -t backup"
To verify your keepalives are working, run a verbose SSH connection and watch for the periodic "debug1: Sending keepalive" messages:
ssh -v user@prod-server
# Wait 60 seconds, you should see:
# debug1: Sending SSH2_MSG_CHANNEL_DATA (keepalive)
Why it works
Firewalls and NAT devices often have idle timeout settings (commonly 300–900 seconds). When no data flows in both directions, they assume the connection is dead and drop the state table entry. ServerAliveInterval forces the SSH client to send small, encrypted packets periodically, keeping the NAT state alive. Meanwhile, tmux or nohup decouples the process lifecycle from the SSH session lifecycle. Even if the TCP connection is dropped, the process continues running in the background, and you can reconnect to inspect it.
Pro Tip
If you’re managing many servers, automate the tmux attach pattern in your shell aliases. Add this to your ~/.bashrc or ~/.zshrc:
alias ssh-attach='ssh $1 "tmux attach -t main 2>/dev/null || tmux new -s main"'
Now, ssh-attach user@prod-server will either attach to an existing main tmux session or create one if it doesn’t exist. This ensures you always have a persistent shell context, even if your local laptop sleeps or your Wi-Fi drops. Also, check your remote sshd_config for ClientAliveInterval. If it’s set to 0 (disabled), the server won’t kick idle clients, but it also won’t send keepalives back. For maximum resilience, set both client and server keepalives to non-zero values (e.g., 60 seconds) to ensure bidirectional liveness detection.