AnsibleXXXIV · Concurrency, Strategies and PerformanceConcurrency, strategies and performance
Pipelining, ControlPersist and connection cost
What you'll learn
- Express connection overhead as a cost per task per host and estimate it for a real play
- State the verified defaults for pipelining, ssh_args and reconnection_retries
- Explain what enabling pipelining requires on every managed node and why that is a fleet-wide change
- Measure a connection change without conflating it with a concurrency change
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
Two earlier lessons in this course established the mechanism: how a module
is packaged and shipped to a target, and what ControlMaster and
ControlPersist remove from that cycle. Neither is repeated here.
This lesson is the performance view of the same machinery — the arithmetic that decides whether connection overhead is your problem, what the settings cost you elsewhere, and how to change one of them without contaminating the measurement.
The arithmetic
Connection overhead is not paid per run or per host. It is paid per task per host, so it scales with the product, and the product is what makes it significant on exactly the plays people complain about.
| Play | Tasks | Hosts | Connection operations |
|---|---|---|---|
| A small role | 12 | 20 | 240 |
| A full site playbook | 60 | 400 | 24,000 |
| A patching run with health checks | 25 | 1,200 | 30,000 |
At 100 ms of overhead per operation — a modest figure for a full SSH
handshake on a LAN, and far too optimistic across a WAN — the second row is
2,400 seconds of overhead distributed across the run. Divided by forks: 20, that is two minutes of pure connection setup, before any task does
anything.
That is the shape to recognise in a profile: time spread evenly across many tasks, none of which individually looks slow. The previous lessons in this part called this “a connection problem wearing a parallelism costume”, and the arithmetic above is how you check whether that is what you have.
The defaults, verified
$ ansible-config dump -t connection | grep -E '^(pipelining|ssh_args|reconnection_retries|timeout|control_path)'pipelining(default) = False
reconnection_retries(default) = 0
control_path(default) = None
control_path_dir(default) = ~/.ansible/cp
ssh_args(default) = -C -o ControlMaster=auto -o ControlPersist=60s
timeout(default) = 10Three of those are worth stating explicitly because they are widely misremembered.
pipelining is false. It is not on by default, and it has never
been. Every claim that Ansible pipelines by default is wrong.
ssh_args already contains connection reuse. ControlMaster=auto and
ControlPersist=60s are in the shipped default, so multiplexing is on
unless something removed it. The most common way something removes it is a
well-meaning ssh_args override that sets a proxy option and drops the
rest of the string.
reconnection_retries is 0, and the plugin is specific about when it
would retry at all:
$ ansible-doc -t connection ansible.builtin.ssh | grep -A5 reconnection_retries reconnection_retries Number of attempts to connect.
Ansible retries connections only if it gets
an SSH error with a return code of 255.
Any errors with return codes other than 255
indicate an issue with program execution.Return code 255 is ssh’s own “something went wrong with the connection”
code. Anything else came from the command that ran on the far side, and
retrying it would be re-running your task, not re-establishing a
connection. That distinction is why the default is 0 and why raising it is
a narrow fix for flaky networks rather than a general reliability setting.
What pipelining requires of your fleet
Pipelining removes the file-transfer step from each task, executing the module over the already-open connection. The plugin describes it as “a very significant performance improvement when enabled”, and then names the condition:
“However this can conflict with privilege escalation (
become). For example, when using sudo operations you must first disablerequirettyin the sudoers file for the target hosts.”
That is the trade the default is protecting you from, and it is worth being precise about why it is a big one.
requiretty in sudoers means sudo refuses to run without a terminal.
It is a hardening measure — its purpose is to make it harder for a
compromised non-interactive process to escalate — and on some
distributions it has historically been set by default.
So enabling pipelining is not a controller-side performance setting. It is
a change to the sudoers policy on every managed node, which means:
- It has to be applied to the whole fleet before the controller setting is
flipped, or the hosts that still have
requirettystart failing. - It is a security-relevant change, which in most organisations means it is reviewed by somebody who is not you.
- It fails on the hosts you forgot: the ones built from an older image, the ones a vendor manages, the ones in the region nobody remembered.
ansible -i inventory/production all -b -m ansible.builtin.command \
-a 'grep -rEl "^[^#]*requiretty" /etc/sudoers /etc/sudoers.d/' \
2>&1 | grep -B1 'rc=0'Hosts that return a match still require a TTY. Every one of them will fail under pipelining, and finding them costs one read-only run.
Measuring the change honestly
for p in false true; do
for pass in 1 2 3; do
printf 'pipelining=%-6s pass=%s ' "$p" "$pass"
ANSIBLE_PIPELINING="$p" /usr/bin/time -f '%e s' \
ansible-playbook -i inventory/staging site.yml --check >/dev/null
done
doneTwo rules, both from the measurement lesson and both worth repeating here because this is the setting people break them on:
Three passes. The run-to-run spread measured earlier in this part was around half a second on a four-second run. A single before-and-after pair proves nothing.
One variable. Do not change forks in the same experiment. They fail
in different places — a saturated controller versus a fleet rejecting
non-TTY sudo — and a combined change leaves you unable to attribute
either the improvement or the regression.
Knowledge check
Knowledge check · 4 questions
Q1. Which statement about ansible-core 2.21.3 defaults is correct?
Q2. Why is enabling pipelining not simply a controller-side performance setting?
Q3. A team enables pipelining fleet-wide and some hosts start failing. Which properties make this hard to diagnose? Select all that apply.
Q4. Connection overhead is paid per task per host, so it scales with the product of the two.
Passing score: 75%. Answers are checked in this browser.