AnsibleXIV · Facts and Registered VariablesFact gathering
What fact gathering actually costs
What you'll learn
- Describe the implicit setup task and where it sits in play execution
- Measure what gathering costs on your own inventory rather than guessing
- Restrict gathering with gather_subset and its negation form
- Decide when gather_facts false is correct and what it breaks
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
Every play you have written so far started with a task you did not write.
PLAY [configure web tier] ******************************************************
TASK [Gathering Facts] *********************************************************
ok: [web01]
ok: [web02]
That task is ansible.builtin.setup, and Ansible inserts it because
gather_facts defaults to true. It runs against every host in the
play, before your first real task, and it does the full work of a
module: build the payload, open a connection, copy, execute under the
target’s Python, parse JSON back.
Most people meet this as a line of output they scroll past. On a fifty-host inventory it is a second or two and nobody notices. On two thousand hosts it is frequently the largest single item in the run, and it happens before you have changed anything at all.
This lesson measures it rather than asserting it, because “gathering is
slow” is the kind of claim that gets repeated into folklore and then
used to justify gather_facts: false everywhere, which breaks things
in ways the next lessons have to clean up.
What the implicit task is
setup is an ordinary module. It runs on the target, collects
information about the system, and returns it under the ansible_facts
key. Ansible then merges that dictionary into the host’s variables,
which is why ansible_facts.distribution is available to every
subsequent task without you doing anything.
Three play keywords control it, and all three are genuine play-level keywords rather than module arguments:
| Keyword | Type | Default | What it does |
|---|---|---|---|
gather_facts | bool | true | Whether the implicit setup task runs at all |
gather_subset | list | all | Which categories of facts to collect |
gather_timeout | int | 10 | Per-subset timeout in seconds |
fact_path | string | /etc/ansible/facts.d | Where custom local facts are read from |
$ ansible-doc -t keyword gather_facts gather_subset gather_timeoutgather_facts:
applies_to:
- Play
description: A boolean that controls if the play will automatically run the 'setup'
task to gather facts for the hosts.
type: bool
gather_subset:
applies_to:
- Play
description: Allows you to pass subset options to the fact gathering plugin controlled
by 'gather_facts'.
type: list
gather_timeout:
applies_to:
- Play
description: Allows you to set the timeout for the fact gathering plugin controlled
by 'gather_facts'.
type: intNote applies_to: Play. These are not task keywords. You cannot put
gather_subset on an individual task — to gather a different subset
mid-play you call the setup module explicitly as a task, which is a
different thing and is covered in lesson 5.
The measurement
Here is the experiment, run on ansible-core 2.21.3. Fifty hosts,
forks: 5, a play whose only real task is a debug that does nothing.
The connection is local, so there is zero network latency — this is
the floor, not a typical case.
$ for p in gather_all gather_min gather_none; do /usr/bin/time -f '%e s' ansible-playbook -i bench.ini -f 5 $p.yml; done--- gather_all (gather_facts: true)
7.65 s
7.20 s
--- gather_min (gather_subset: ['!all'])
3.50 s
3.19 s
--- gather_none (gather_facts: false)
0.40 s
0.42 sRead that carefully. The play that does nothing takes 0.4 seconds. Add full fact gathering and it takes 7.2 seconds. Gathering is 94% of the run.
The minimal subset — everything except the expensive collectors — costs 3.2 seconds, so restricting the subset saved more than half the gathering time without disabling it.
Where the time goes
The subsets are not equal. Counting the facts each one returns shows where the weight sits:
$ for s in all '!all' '!all,!min' network hardware; do ansible localhost -m ansible.builtin.setup -a "gather_subset=$s"; doneall -> 121 keys
!all -> 55 keys
!all,!min -> 2 keys
network -> 76 keys
hardware -> 90 keysFull gathering returns 121 top-level facts. The min subset — what you
get from !all — returns 55, and covers essentially everything a
typical play branches on: distribution, kernel, hostname, service
manager, package manager, Python, user identity, SSH host keys.
The remaining 66 facts are the expensive ones. hardware enumerates
block devices, mounts and memory. network enumerates every interface
and address. Those two collectors are where the seconds are, and they
are also the two that most plays never read.
Verified against 2.21.3:
$ ansible localhost -m ansible.builtin.setup -a "gather_subset=!all,!min,distribution"localhost | SUCCESS => {
"ansible_facts": {
"ansible_distribution": "Ubuntu",
"ansible_distribution_file_parsed": true,
"ansible_distribution_file_path": "/etc/os-release",
"ansible_distribution_file_variety": "Debian",
"ansible_distribution_major_version": "26",
"ansible_distribution_release": "resolute",
"ansible_distribution_version": "26.04",
"ansible_os_family": "Debian",
"gather_subset": [
"!all",
"!min",
"distribution"
],
"module_setup": true
},
"changed": false
}Writing it in a play
Restricting the subset is a two-line change at play level:
- name: Deploy the agent
hosts: appservers
gather_subset:
- '!all'
- '!min'
- distribution
- pkg_mgr
tasks:
- name: Install the agent package
ansible.builtin.package:
name: monitoring-agent
state: presentTwo habits make this maintainable rather than a micro-optimisation someone reverts in six months:
Quote the negations. !all starts with ! and YAML will not parse
it bare in some contexts. Always quote subset entries that begin with
!. This is the same YAML rule covered in the YAML part, and it bites
here more often than anywhere else.
Write down which facts the play needs. A comment naming the facts
the play reads turns the subset list from a magic incantation into
something a reviewer can check. When someone later adds a task that
reads ansible_facts.mounts, the subset list is where the bug will be,
and a comment is what makes that obvious.
gather_timeout and the host that hangs
gather_timeout defaults to 10 seconds and applies per fact
collector, not to the gathering task as a whole.
That default is usually right and occasionally catastrophic. The
classic case is hardware on a host with a hung NFS mount: the
collector walks the mount table, stat() blocks on the dead mount, and
the collector burns its full ten seconds before giving up. Multiply by
the number of affected hosts, divide by forks, and a routine run
becomes a long one.
Raising gather_timeout is almost always the wrong fix. It makes the
run slower and hides the real problem, which is a broken mount. The
right responses, in order of preference:
- Fix the host. A hung mount is an incident, not a gathering setting.
- Exclude the collector you do not need —
'!hardware'— so the play stops touching the broken thing at all. - Raise the timeout only if you genuinely need the facts from a host that is legitimately slow to answer.
When gather_facts: false is the right call
Disabling gathering entirely is correct more often than its reputation suggests, and wrong in one specific way that costs people an afternoon.
Correct when the play reads no facts. A play that restarts a
service by name, or copies a file to a fixed path, or runs a health
check against a URL, reads nothing the setup module produces.
Gathering for it is pure cost. This is most of what an operational
runbook play does.
Correct on a host with no Python. setup needs a Python on the
target. Bootstrap plays that install Python with raw must disable
gathering, because the gathering task would fail before the bootstrap
task ran. This is covered in the interpreter part; the gathering
consequence belongs here.
Correct when facts come from cache. If a fact cache is configured
and populated, gathering: smart will skip the gathering task for hosts
it already has facts for. That is lesson 5, and it is the setting that
lets you keep facts and stop paying for them on every run.
Wrong when a module gathers on your behalf. This is the one that
catches people. Upstream documents it plainly for
ansible.builtin.package: it selects the underlying package manager
from facts, and “if ansible.builtin.setup was not yet run,
ansible.builtin.package will run it”.
So a play with gather_facts: false whose first task is package
gathers facts anyway — just later, and without any of the subset
restrictions you set at play level, and with the cost hidden inside a
task that appears to be doing something else. The run is not faster. It
is the same cost, harder to see.
A decision you can apply
For any play, in order:
- Does it read facts at all? Grep the play and its roles for
ansible_factsandansible_. If nothing,gather_facts: falseand move on — but check for apackage,serviceorsetuptask that will gather implicitly. - Which facts does it read? Map each to a collector. Distribution
branching needs
distribution. Choosing a package manager needspkg_mgr. Interface addresses neednetwork. Disk checks needhardware. - Set the subset explicitly, with a comment naming why.
- Measure, on your own inventory, before and after. If the saving is a tenth of a second, put the default back and spend the attention elsewhere.
Step 4 matters as much as the rest. Restricting the subset on a twelve-host inventory is a change with a cost — a future reader has to understand it — and no benefit. Fleet-scale reasoning applied at small scale is just complexity.
Knowledge check
Knowledge check · 4 questions
Q1. A play sets gather_facts: false and its first task uses ansible.builtin.package to install a package. What actually happens to fact gathering?
Q2. What does gather_subset: ['!all'] collect?
Q3. A fleet run is slow and gathering is suspected. Which of these are sound next steps? Select all that apply.
Q4. gather_subset is a play-level keyword and cannot be set on an individual task.
Passing score: 75%. Answers are checked in this browser.