---
title: "Why Idle SSH Sessions Freeze and Drop (and How Keep-Alives Fix It)"
date: 2026-10-24T10:00:00.000Z
author: Z.SHINCHVEN
tags: [ssh, networking, nat, linux, guide]
canonical: https://atlassc.net/2026/10/24/why-idle-ssh-sessions-freeze-and-drop
---
You open an SSH session, step away for a coffee, come back, and type a command. Nothing happens. The terminal just hangs. A few minutes later it finally gives up with `client_loop: send disconnect: Broken pipe` or `Connection reset by peer`.

Nothing is wrong with the server, and nothing is wrong with your SSH client. The connection was quietly killed somewhere in the middle of the network while you weren't looking.

## The Culprit: NAT Session Timeouts

Almost every consumer internet connection sits behind NAT (Network Address Translation). Your router keeps a table that maps each outgoing connection from your machine to a port on its public IP address. Many ISPs add a second layer on top of that, called **CGNAT** (Carrier-Grade NAT), which shares one public address among many customers.

Those NAT tables take up memory, and consumer routers and CGNAT boxes are built to be cheap. To keep the table from filling up, they evict entries that look abandoned. A TCP connection that hasn't carried a single packet for a while looks abandoned, and an idle SSH session sends no packets at all.

How long "a while" is depends on the device:

| Network gear | Typical idle TCP timeout |
| --- | --- |
| Enterprise firewalls and data-center gateways | Hours, sometimes days |
| Consumer routers | Often 5 to 30 minutes |
| Carrier-grade NAT | Can be just a few minutes |

That explains why the same SSH workflow can run fine on one network for days and fall apart on another within half an hour. The gear in between has much less patience.

## Why It Freezes Instead of Disconnecting

When the router evicts your connection, **it tells nobody**. Neither your laptop nor the server gets a notification. Both ends still believe the TCP connection is alive.

When you come back and press a key:

1. Your client sends the keystroke into a connection that no longer exists.
2. The router has no mapping for it, so the packet is dropped (or, on some devices, answered with a TCP RST).
3. If the packet is silently dropped, your OS keeps retransmitting with exponential backoff, waiting for an ACK that will never arrive.
4. Only after the kernel's retransmission timeout runs out, which can take several minutes, does SSH learn the connection is dead and print `Broken pipe`.

That's the freeze: your terminal is waiting on TCP retransmits.

## The Fix: SSH Keep-Alives

The solution is to make sure the connection is never idle. If the client sends a tiny packet every minute, the NAT entry stays fresh and never gets evicted.

OpenSSH has this built in through `ServerAliveInterval`.

### Option 1: Set It Globally (Recommended)

Edit (or create) `~/.ssh/config` on your local machine:

```ssh-config
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
```

- **`ServerAliveInterval 60`**: if no data has been received from the server for 60 seconds, the client sends a keep-alive message through the encrypted channel and asks for a reply. Sixty seconds is well under any realistic NAT timeout, so the mapping is always kept alive.
- **`ServerAliveCountMax 3`**: if three keep-alives in a row go unanswered (about 3 minutes), the client disconnects on its own. When the network really is down, you get a clean exit instead of a terminal frozen indefinitely.

If your NAT is unusually aggressive, lower the interval to 30 or even 15 seconds. The traffic involved is negligible.

### Option 2: One-Off on the Command Line

If you can't or don't want to touch the config file:

```bash
ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=3 user@your-server
```

### Option 3: Configure It on the Server

If you administer the server and want to protect every client, including ones you don't control, set the server-side equivalent in `/etc/ssh/sshd_config`:

```ssh-config
ClientAliveInterval 60
ClientAliveCountMax 3
```

Then reload the daemon (`sudo systemctl reload ssh` or `sshd`, depending on your distro). This has the server ping the client instead, which keeps the NAT entry alive just as well.

### What About `TCPKeepAlive`?

`TCPKeepAlive yes` is already the default, but it doesn't help here. It relies on kernel-level TCP keepalives, which on most systems don't start until **2 hours** of idle time, long after a consumer router has dropped your mapping. `ServerAliveInterval` works at the SSH protocol level with timing you choose, so that's the setting to use.

## Belt and Braces: Run Your Work Inside tmux

Keep-alives stop NAT timeouts, but they can't help when the network itself goes down: a Wi-Fi drop, a router reboot, or your ISP reassigning your public IP. In those cases the TCP connection is gone for good, and any process attached to that SSH session dies with it.

The answer is to keep your work in a session that lives on the server, independent of your connection. `tmux` is the usual choice (`screen` works too):

```bash
# On the server, start a named session
tmux new -s work

# ...the connection drops...

# Reconnect over SSH, then pick up exactly where you left off
tmux attach -t work
```

Running processes, scrollback, working directories, and split panes all survive the disconnect. For long builds, data migrations, or anything else you don't want to restart, this is the real safety net.

If you're on unreliable connections a lot, take a look at [Mosh](https://mosh.org/) too. It runs over UDP, survives IP changes and roaming between networks, and pairs nicely with tmux.

## TL;DR

- Idle SSH sessions drop because NAT devices silently evict connections that haven't sent traffic in a while.
- The terminal freezes because neither end is told; it's waiting on TCP retransmits to time out.
- Add `ServerAliveInterval 60` and `ServerAliveCountMax 3` to `~/.ssh/config` so the connection never goes idle.
- Run long-lived work inside `tmux` so a real network outage doesn't take your session with it.
