Why SSH keeps spawning new processes on every connect
Every time I run ssh host, the client forks a new process that talks to the remote sshd.
On the server side, sshd spawns a child for each accepted connection.
The model is simple and works fine for a handful of sessions, but it starts to feel like a drain when you:
- run a script that opens dozens of SSH connections in a row,
- use CI/CD pipelines that hit a remote host repeatedly, or
- have a home‑lab with many small services that need quick SSH access.
Each fork eats a few kilobytes of RAM and a handful of file descriptors.
If you hit the per‑user process limit (ulimit -u) or the system’s max_user_processes, you’ll see “Too many users” or “Connection refused” errors.
Even on a modest machine, the cumulative CPU cost of negotiating the SSH handshake over and over can add up.
ControlPersist solves it
ControlPersist is a client‑side feature that keeps a master SSH connection alive in the background.
Subsequent ssh invocations reuse that socket instead of opening a brand‑new TCP session.
# ~/.ssh/config
Host *
ControlMaster auto
ControlPath ~/.ssh/cm_%h_%p_%r
ControlPersist 10m
ControlMaster auto– start a master if none exists, otherwise reuse it.ControlPath– the Unix domain socket that the master listens on.ControlPersist 10m– keep the master alive for 10 minutes after the last client exits.
With this in place, a quick test shows the difference:
$ time ssh -vv remote-host uptime
...
# 0.12s elapsed
$ time ssh -vv remote-host uptime
...
# 0.04s elapsed
The second command reuses the existing socket, so the handshake is skipped and the command runs faster.
Practical usage
- One‑off scripts: add
-o ControlMaster=auto -o ControlPersist=5mto the command line.
No need to touch~/.ssh/config. - Interactive work: keep the master running in the background by simply opening a terminal and running
ssh remote-host.
Subsequent commands will be instant. - Cleanup: when you’re done, close the master with
ssh -O exit remote-host.
Or let the 10‑minute timeout expire.
Caveats
| Issue | What to watch for | Fix |
|---|---|---|
| Stale sockets | If the master crashes, the socket file remains and new clients fail to connect. | ssh -O exit remote-host or delete ~/.ssh/cm_*. |
| File‑descriptor limits | The master keeps file descriptors open for each session. | Use ControlPersist 0 to disable persistence, or increase MaxSessions on the server. |
| Server support | Some older sshd versions ignore ControlPersist‑related options. |
Ensure you’re running OpenSSH ≥ 7.0 (most distros ship newer). |
| Security | The socket is world‑writable by default on some systems. | Set chmod 700 ~/.ssh/cm_* or use ControlPath ~/.ssh/cm_%h_%p_%r which creates the socket with the client’s UID. |
Security note
The Unix domain socket is the only surface that persists after the client exits.
Make sure it’s owned by you and not group‑writable:
chmod 700 ~/.ssh/cm_*
If you’re using sudo to run SSH commands, the socket will be owned by root, which can be a privilege escalation vector.
Avoid running ssh as root unless absolutely necessary.
Bottom line
ControlPersist turns a stateless SSH client into a lightweight multiplexing proxy.
It cuts down on CPU, memory, and network chatter, and it keeps your process limits from blowing out in busy scripts or homelab setups.
Add the snippet to ~/.ssh/config, test with ssh -vv, and you’ll notice the difference immediately.
See also
- Repairing a broken initramfs that locked my Raspberry Pi in emergency mode after a kernel update
- How I added ionice ‑c3 to my nightly rsync backup and finally stopped my laptop from overheating
- Running a nightly backup script after logout with systemd user units – no nohup needed
- I lost SSH access to my home server after accidentally setting /home/user to 777 – how I restored it with a live USB
- When pulseaudio turns your laptop into a CPU hamster: how to reduce its load with a simple configuration tweak