Skip to main content
RunBook Academy

AnsibleXXXIV · Concurrency, Strategies and PerformanceConcurrency, strategies and performance

Pipelining, ControlPersist and connection cost

Advanced⏱ ~26 minansible-coreopenssh-client

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

Not yet marked complete on this device.

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.

PlayTasksHostsConnection operations
A small role1220240
A full site playbook6040024,000
A patching run with health checks251,20030,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

Read-only / Safewhat the ssh connection plugin actually defaults to
$ 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) = 10

Three 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:

Read-only / Safewhen Ansible retries a connection
$ 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 disable requiretty in 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 requiretty start 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.
Read-only / Safecheck the precondition before flipping the switch
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

Read-only / Safeone variable, three passes each
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
done

Two 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

  1. Q1. Which statement about ansible-core 2.21.3 defaults is correct?

  2. Q2. Why is enabling pipelining not simply a controller-side performance setting?

  3. Q3. A team enables pipelining fleet-wide and some hosts start failing. Which properties make this hard to diagnose? Select all that apply.

  4. 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.