AnsibleXXXIV · Concurrency, Strategies and PerformanceConcurrency, strategies and performance
Fact gathering is usually the bill
What you'll learn
- Audit a repository for the facts it actually reads before removing any gathering
- Compare the measured cost of the gather_subset choices and pick one from evidence
- Gather facts on demand mid-play with ansible.builtin.setup where only some hosts need them
- Use the gathering setting to change the fleet-wide default without editing every play
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
The facts part of this course established what the implicit setup task
is, what it costs, and how gather_subset and gather_facts: false change
that. This lesson does not repeat the mechanism. It answers the question
that comes up once you have a profile in front of you: given that
gathering is the top line, how do you cut it without breaking something?
That question has a specific answer, and it is not “try it and see”. Facts
are read implicitly all over a mature repository — in when: conditions,
in templates, in role defaults, in variable definitions two layers away
from the play — and removing one is not a change that fails loudly. It is a
change that renders a config file with an empty value.
Why this lesson leads the tuning list
The measurement from the first lesson in this part, on a play that gathers facts and prints one value across 24 hosts:
$ ANSIBLE_CALLBACKS_ENABLED=ansible.posix.profile_tasks ansible-playbook -i facts.ini facts.yml -f 12TASKS RECAP ********************************************************************
===============================================================================
Gathering Facts --------------------------------------------------------- 2.43s
Nothing else ------------------------------------------------------------ 0.12s95% of the run, with the network removed. On a real fleet the gathering task also pays an SSH conversation per host, so the proportion goes up, not down.
Every other lever in this part trades something away. Raising forks
trades an accidental blast-radius limit. free trades the synchronisation
barrier. Pipelining trades a sudoers hardening default. Fact caching
trades freshness.
Cutting gathering trades nothing — provided the facts you cut are ones nothing reads. That proviso is the whole lesson.
The audit
Before changing any play, find out what the repository actually reads.
# 1. Direct fact references, in every form they are written
grep -rhoE 'ansible_facts\[.[a-z_]+|ansible_facts\.[a-z_]+|\bansible_[a-z0-9_]+' \
roles/ playbooks/ inventory/ templates/ 2>/dev/null \
| sed -E 's/ansible_facts[\.\[].?//' \
| sort -u
# 2. Anything reading facts of OTHER hosts - these need those hosts gathered
grep -rn 'hostvars\[' roles/ playbooks/ 2>/dev/null
# 3. Templates are the easiest place to miss one
grep -rn 'ansible_' templates/ roles/*/templates/ 2>/dev/nullRun it and read the output. A typical result on a real repository is
twenty to forty distinct facts, and almost all of them come from three or
four subsets — distribution, pkg_mgr, default_ipv4, processor_count.
The list is nearly always much shorter than people expect, which is exactly
why the saving is available.
Map what you find onto subsets with ansible-doc, which lists every legal
value:
ansible-doc ansible.builtin.setup | sed -n '/gather_subset/,/^$/p'
What each subset costs
Measured on the pinned controller: 24 hosts, forks: 12, one trivial task,
with only the gathering configuration changed.
$ /usr/bin/time -f '%e s' ansible-playbook -i facts.ini gsub.yml -f 12 -e '{"gs":[...]}'gather_facts: false 0.35 s
gather_subset: ['!all','!min','distribution'] 1.08 s
gather_subset: ['!all'] (min) 1.10 s
gather_subset: ['!all','!min','network'] 1.39 s
gather_subset: ['!all','!min','hardware'] 2.53 s
gather_subset: ['all'] (default) 2.81 sFour readings from that table:
- Full gathering is 2.46 s of a 2.81 s run. 87% of the wall clock, before the network.
hardwareis the expensive one. 2.53 s on its own — nearly the whole cost ofall. It walks the mount table, enumerates devices and reads memory and CPU topology. If nothing in your repository readsansible_memtotal_mb,ansible_processor_count,ansible_devicesoransible_mounts, this is the single biggest saving available.- The minimal set is cheap. 1.10 s, and it includes
distribution, which is what most conditionals actually branch on. Note that['!all','!min','distribution']measured 1.08 s — the same asmin— because!allalone still collects the minimal set. - Disabling gathering entirely is a step change, not an increment: 0.35 s. If a play genuinely reads no facts, that is the option.
- name: Deploy the monitoring agent
hosts: appservers
# Audited 2026-08-11: this play and the roles it includes read
# ansible_distribution, ansible_os_family and ansible_pkg_mgr only.
# hardware is not collected - it was 2.53s of a 2.81s run.
gather_subset:
- '!all'
- '!min'
- distribution
- pkg_mgr
tasks:
- name: Install the agent package
ansible.builtin.package:
name: monitoring-agent
state: presentThe comment is the load-bearing part. Without it, the next person to add a
task that reads ansible_mounts gets an undefined-variable error with no
indication of where the constraint came from, and the obvious fix — deleting
the gather_subset block — throws away the saving instead of extending the
list.
Gathering on demand
The third option, and the one that fits a play where some hosts need facts and most do not, or where facts are needed only after something has changed.
ansible.builtin.setup is an ordinary module. You can call it whenever you
like:
- name: Resize and reconfigure
hosts: appservers
gather_facts: false # most of this play needs nothing
tasks:
- name: Expand the data volume
community.general.lvol:
vg: data
lv: app
size: "{{ app_volume_size }}"
register: volume_change
- name: Re-read hardware facts now that the volume changed
ansible.builtin.setup:
gather_subset:
- '!all'
- '!min'
- hardware
when: volume_change is changed
- name: Size the cache to the volume we now actually have
ansible.builtin.template:
src: cache.conf.j2
dest: /etc/app/cache.conf
mode: '0644'
when: volume_change is changedTwo things this shape gets right that a play-level gather_facts: true
cannot.
The expensive hardware subset is collected only on hosts where something
changed, rather than on all of them at the start. And the facts are read
after the change rather than before, which is the difference between
sizing a cache to the new volume and sizing it to the old one.
Knowledge check
Knowledge check · 4 questions
Q1. Measured across 24 hosts with local connections: full gathering 2.81s, hardware alone 2.53s, minimal 1.10s, gathering disabled 0.35s. A play branches only on ansible_os_family and ansible_pkg_mgr. What is the best-supported change?
Q2. Which of these is NOT evidence that a play needs fact gathering?
Q3. Why is restricting fact gathering described as the cheapest large win in this part? Select all that apply.
Q4. With the default gathering setting, configuring a fact cache means facts are read from the cache instead of being gathered.
Passing score: 75%. Answers are checked in this browser.