SSH ControlMaster and ControlPersist: A Deep Guide to Connection Multiplexing

Reuse one authenticated SSH connection for every ssh, scp, rsync, and git command. Covers ControlPath, ssh -O, hung masters after sleep, MaxSessions, and scripts.

SSH connection multiplexing lets every ssh, scp, sftp, rsync, and git command to a host share one TCP connection that has already completed key exchange and authentication. Enable it with three settings in ~/.ssh/config:

  • ControlMaster auto
  • a private ControlPath built from %C
  • a ControlPersist timeout

After the first connection, later ones skip the handshake and any second-factor prompt, and they start almost immediately.

The configuration fits in a few lines. Most of this guide covers what comes after: how to inspect and control the shared connection, why multiplexed sessions hang after your laptop sleeps, what Session open refused by peer means, and how to use multiplexing safely in scripts.

What a master connection is

Without multiplexing, every ssh invocation opens its own TCP connection, negotiates encryption, verifies the host key, and authenticates.

With multiplexing, the first invocation becomes the master. It does all of that once, then listens on a Unix domain socket called the control socket. Later invocations to the same destination become clients. They find the socket, pass their request to the master, and the master opens a new channel inside its existing connection.

The SSH protocol was designed for this. One connection can carry many independent channels: interactive shells, single commands, subsystems such as SFTP, and port forwards. Multiplexing lets separate processes on your machine share those channels.

What you gain:

  • Latency. Clients skip the TCP handshake, key exchange, and authentication. On a distant server, or with a hardware security key, that is the difference between a noticeable pause and an immediate prompt.
  • Fewer authentication prompts. You touch your key or approve a push once per master, not once per command.
  • Fewer simultaneous connections. sshd's MaxStartups setting limits concurrent unauthenticated connections. With the default 10:30:100, sshd starts refusing a growing share of new connections once 10 are mid-handshake. Deploy scripts and tools that fan out in parallel can hit that limit. Multiplexed sessions are channels on an existing connection, so they never count.

Measure the difference on your own hosts:

time ssh dev.example.com true    # first run: full handshake, becomes the master
time ssh dev.example.com true    # second run: reuses the master

Configuration

# ~/.ssh/config
Host dev.example.com *.internal.example.com
    ControlMaster auto
    ControlPath ~/.ssh/cm/%C
    ControlPersist 10m
    ServerAliveInterval 15
    ServerAliveCountMax 3

Create the socket directory and keep it private:

mkdir -p ~/.ssh/cm
chmod 700 ~/.ssh/cm

ControlMaster auto

Use an existing master if one is listening; otherwise become the master. The default is no, which disables multiplexing.

ControlPath ~/.ssh/cm/%C

The location of the control socket. %C is a hash of the local host name, remote host name, port, remote user, and, in current OpenSSH, the ProxyJump hosts. In ssh_config token terms that is %l%h%p%r%j.

The hash has two advantages over spelling out %r@%h:%p:

  • Different users, ports, or jump paths never share a socket by accident.
  • The file name has a short, fixed length. Unix socket paths are limited to 104 bytes on macOS and 108 on Linux. A long home directory plus a long host name can exceed that and fail with path "…" too long for Unix domain socket.

The ssh_config manual recommends a directory that other users cannot write to.

ControlPersist 10m

Keep the master running in the background for 10 minutes after its last client disconnects. yes keeps it until you stop it, and other time formats such as 1h work too.

The default, no, is surprising in practice. Your first interactive session becomes the master and cannot fully exit while other sessions depend on it. Closing that terminal looks like a hang until every other session ends. With ControlPersist, the master moves to the background immediately and your first session behaves like any other client.

ServerAliveInterval 15 and ServerAliveCountMax 3

If the server sends nothing for 15 seconds, ssh sends an encrypted keepalive. After 3 unanswered keepalives, about 45 seconds, it disconnects. The interval defaults to 0, which disables the check. With multiplexing this is essential, as the failure modes below show.

Scope it deliberately

You can put these settings under Host *, but naming hosts is better. Leave multiplexing off where each session should authenticate separately, and on machines other people administer.

ssh -G dev.example.com prints the effective configuration for a host, which helps when several Host blocks match.

Inspect and control masters with ssh -O

ssh -O check dev.example.com    # Master running (pid=48213)
ssh -O exit dev.example.com     # close the master and every session using it
ssh -O stop dev.example.com     # accept no new clients; current sessions continue

-O commands talk to the local master through its socket, so they must resolve to the same ControlPath: same host alias, user, port, and jump hosts. If nothing is listening, check reports Control socket connect(…): No such file or directory.

stop is useful after a configuration change. Current sessions finish on the old master, and the next connection builds a new one with the new settings.

You can add and remove port forwards on a running master without opening a shell:

ssh -O forward -L 5432:127.0.0.1:5432 dev.example.com
ssh -O cancel  -L 5432:127.0.0.1:5432 dev.example.com

Our port forwarding guide explains the -L syntax and when to use each forward type.

To see which masters exist, list the socket directory and the processes. A backgrounded master has [mux] in its process title:

ls -l ~/.ssh/cm/
ps -eo pid,args | grep '\[mux\]'

To bypass multiplexing for one command, use ssh -S none dev.example.com. Do this when debugging authentication or host keys. A client that reuses a master never authenticates, so ssh -v through a master tells you nothing about authentication.

ssh -v does show whether multiplexing is in use. Look for auto-mux: Trying existing master at '…' in the debug output.

Failure modes and fixes

Sessions hang after sleep or a network change

Symptom: ssh dev.example.com prints nothing and waits. git fetch and rsync hang too. ssh -O check still reports Master running.

Cause: The master process is alive on your machine, but its TCP connection died while the laptop slept, changed Wi-Fi networks, or reconnected a VPN. The master has not noticed yet. New clients connect to the master successfully, ask for a channel, and wait for a reply that will never arrive.

-O check asks the local process whether it is running. It does not test the path to the server, so it cannot catch this.

Immediate fix: run ssh -O exit dev.example.com. It only talks to the local master, so it works when the network path is dead. Then reconnect. If the master does not respond, find its PID with the ps command above, stop it, and remove its socket.

Prevention: ServerAliveInterval 15 with ServerAliveCountMax 3. The master detects an unresponsive server after about 45 seconds and exits, and the next command builds a fresh connection.

Lower values detect failures sooner, at the cost of more traffic and occasional false disconnects on very lossy links. TCPKeepAlive, which is on by default, works at the TCP level, and operating-system keepalive timers typically wait hours before probing. Do not rely on it for this.

A stale socket after a crash

If a master is killed, its socket file can remain. Current OpenSSH notices that nothing is listening, removes the stale socket, and connects normally. ssh -v logs this as Stale control socket …, unlinking.

Older versions print Control socket connect(…): Connection refused. With ControlMaster auto, they then print ControlSocket … already exists, disabling multiplexing and connect without multiplexing. Deleting the socket file fixes it.

Session open refused by peer

Symptom: a new session fails with mux_client_request_session: session request failed: Session open refused by peer, typically once you have about ten sessions open to one host.

Cause: sshd's MaxSessions setting, which defaults to 10, limits shell, command, and subsystem sessions per network connection. With multiplexing, every terminal, every SFTP session from a file browser, and every helper an editor or agent starts shares one connection, and therefore one limit. Port forwards do not count. The sshd_config manual notes that MaxSessions 1 effectively disables multiplexing.

Fixes:

  • If you administer the server, raise MaxSessions.
  • Give heavy users their own connection with a separate path, for example ssh -o ControlPath=~/.ssh/cm/%C-bulk.
  • Bypass multiplexing for batch jobs with -S none.
  • Cap the parallelism of fan-out scripts below the limit.

Changed settings do not take effect

Clients reuse the master's connection as it is. Anything decided when that connection was set up stays in force until the master exits: which key or certificate authenticated, the negotiated ciphers, compression. Settings that change %C, such as the user, port, or jump host, create a separate master instead.

If you rotate a key, change IdentityFile, or adjust a connection setting and nothing seems different, run ssh -O exit and connect again.

Security considerations

A control socket is an authenticated session. Any process that can connect to it can run commands on the server without your key, password, or second factor. OpenSSH accepts clients only from the same user, or root, and the private directory keeps everyone else from reaching the socket at all. Processes running as you can still use it.

Two conclusions follow:

  • Choose ControlPersist with your threat model in mind. A ten-minute idle window is a different risk from yes.
  • Avoid persistent masters on shared machines, such as bastion hosts administered by others.

Multiplexing in scripts

A deploy script that runs many commands against one host can start a dedicated master and remove it even when the script fails. This does not depend on the user's ~/.ssh/config, so it also works in CI:

#!/usr/bin/env bash
set -euo pipefail

host="deploy@app-1.example.com"
ctl_dir="$(mktemp -d)"
ctl="$ctl_dir/cm"

cleanup() {
  ssh -o ControlPath="$ctl" -O exit "$host" 2>/dev/null || true
  rm -rf "$ctl_dir"
}
trap cleanup EXIT

# Authenticate once, then background the master without running a command.
ssh -o ControlMaster=yes -o ControlPath="$ctl" -o ControlPersist=no -fN "$host"

ssh -o ControlPath="$ctl" "$host" 'mkdir -p /srv/app/releases/next'
rsync -a --delete -e "ssh -o ControlPath=$ctl" ./build/ "$host:/srv/app/releases/next/"
ssh -o ControlPath="$ctl" "$host" 'ln -sfn /srv/app/releases/next /srv/app/current'

-f sends the master to the background after authentication, and -N tells it not to run a remote command. It lives until cleanup sends -O exit.

mktemp -d returns short paths without spaces on macOS and Linux, which keeps $ctl under the socket path limit and safe inside the unquoted rsync -e string.

Git can use the same master:

GIT_SSH_COMMAND="ssh -o ControlPath=$ctl" git push origin main

When you fan out commands in parallel, every concurrent command is a session on the same connection. Cap concurrency below the server's MaxSessions, for example with xargs -P 8.

Multiplexing with ProxyJump

With ProxyJump bastion, a separate ssh process makes the hop to the bastion, and it reads the same configuration. If the bastion's Host block enables multiplexing, every connection through it shares one bastion master, so you authenticate to the bastion once.

Because %C includes the jump hosts, a direct connection and a jumped connection to the same target get different sockets. They cannot be confused.

Related reading

The SSH file transfer guide shows where multiplexing speeds up repeated scp and rsync runs. If transfers fail with protocol errors before multiplexing even matters, read why shell startup output breaks scp, rsync, and SFTP.

Agents UI's SSH file browser and file transfers reuse a shared connection master in the same way. Version 0.12.0 recovers that shared connection after sleep, network changes, or a reconnect, which is the hung-master problem described above.

Try Agents UI

A native terminal for AI coding agents with persistent sessions, SSH workflows, and built-in editing.