Skip to main content
RunBook Academy

AnsibleXXXIV · Concurrency, Strategies and PerformanceConcurrency, strategies and performance

Fact gathering is usually the bill

Advanced⏱ ~25 minansible-coregrep

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

Not yet marked complete on this device.

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:

Read-only / Safethe profile that starts most investigations
$ ANSIBLE_CALLBACKS_ENABLED=ansible.posix.profile_tasks ansible-playbook -i facts.ini facts.yml -f 12
TASKS RECAP ********************************************************************
===============================================================================
Gathering Facts --------------------------------------------------------- 2.43s
Nothing else ------------------------------------------------------------ 0.12s

95% 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.

Read-only / Safewhat facts does this repository consume?
# 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/null

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

Read-only / Safethe cost of each gathering choice
$ /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 s

Four readings from that table:

  • Full gathering is 2.46 s of a 2.81 s run. 87% of the wall clock, before the network.
  • hardware is the expensive one. 2.53 s on its own — nearly the whole cost of all. It walks the mount table, enumerates devices and reads memory and CPU topology. If nothing in your repository reads ansible_memtotal_mb, ansible_processor_count, ansible_devices or ansible_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 as min — because !all alone 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.
Read-only / Safea subset chosen from an audit, with the reason recorded
- 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: present

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

Read-only / Safegather late, and only where needed
- 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 changed

Two 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

  1. 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?

  2. Q2. Which of these is NOT evidence that a play needs fact gathering?

  3. Q3. Why is restricting fact gathering described as the cheapest large win in this part? Select all that apply.

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