Chapter 5: Variables & Facts
Topic 31

Fact Caching

FactsPerformance

Fact gathering happens per play, so a play that targets only web1.larkspur.io never gathers db1's facts — and the moment web1 tries to read hostvars['db1.larkspur.io'], those facts are absent. Fact caching solves this by persisting gathered facts in a backend — a JSON file tree or Redis — so any later run can read a host's facts without that host being in the current play.

The trade is freshness for speed: cached facts are as old as the cache, and a stale cache feeds yesterday's IP into today's template. Caching never decides what to create or destroy — it only avoids re-asking a host that recently answered — so it cannot make Ansible delete the wrong resource the way a corrupt state file can. A stale fact can still feed a wrong value into a when or a template, though, rendering the wrong config or taking the wrong branch — a wrong outcome, just not an infrastructure-lifecycle one. This topic is where the cross-host read from Topic 30 becomes reliable for targeted plays.

The Problem Caching Solves

Without a cache, cross-host reads only work for hosts gathered in the same play. A targeted web1-only deploy that templates db1's IP into app.env fails, because db1 was never touched this run and its facts are undefined in hostvars. You hit this the first time you run a focused, single-host deploy that still needs a peer's address — the play that worked against the whole fleet breaks the moment you narrow it.

Caching makes the absent host's last-known facts available anyway. With a cache configured, db1's facts from its most recent gather sit in the backend, and web1's template reads them even though db1 isn't in this play. The cross-host read stops depending on both hosts being in the same run, which is what makes targeted deploys against a real topology workable.

How a cached fact survives into a later run
run 1 gathers facts
cache (jsonfile/redis)
run 2 reads the cache
no re-gather

Cache Backends — jsonfile and redis

fact_caching = jsonfile writes one JSON file per host under a configured directory — simple, repo-local, no server to run, ideal for a single control node. fact_caching = redis stores facts in Redis, shared across control nodes and CI runners with a built-in TTL, which is what you want when more than one machine runs plays and they need to see the same cached facts. Both are set in ansible.cfg alongside fact_caching_connection and a timeout.

ansible.cfg — jsonfile caching with smart gathering and a one-hour TTL
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /var/tmp/ansible_facts
fact_caching_timeout = 3600

The choice is about how many machines share the cache. One control node is happy with jsonfile pointed at a writable directory; a fleet of CI runners needs redis so a gather on one runner is visible to the next. The fact_caching_connection is the directory for jsonfile or the server address for redis, and the timeout governs freshness for both.

Gather Only What's Stale

With caching on and gathering = smart, Ansible gathers a host's facts only if the cache lacks them or they have expired, and skips the round-trip for hosts whose cached facts are still fresh. This is the speed payoff: a large fleet stops re-gathering every host on every run and pays only for the hosts whose cache is missing or stale. On a hundred-host inventory where most facts rarely change, that turns a slow gather into a near-instant one.

smart is the gathering mode that makes caching worth configuring for performance rather than just for cross-host reads. Without it, gathering runs every play regardless of the cache; with it, the cache becomes a skip list, and the per-host setup cost is paid once per TTL window instead of once per run. The two settings — caching plus smart — are what convert a large, slow fleet into a fast one.

Freshness vs Speed — the Cache TTL

fact_caching_timeout sets how long cached facts stay valid. A long TTL means fast runs — most hosts skip gathering — but risks templating a stale IP after a host changed addresses, with no error to warn you. A short TTL means fresher data and more frequent re-gathering, trading some speed for accuracy. You tune it to how often the facts you actually read change: stable hardware facts tolerate a long TTL, volatile network facts want a short one.

The danger case is concrete. Set a multi-hour TTL, let db1 change its IP after a reboot, and a web1 deploy inside the TTL window renders db1's old address into app.env — pointing the application at a box that moved, silently, because the cache still holds yesterday's value. There is no failure; the config is simply wrong until the cache expires. Matching the TTL to the volatility of the facts you read is how you avoid it.

Caching and the Stateless Spine

Caching is a read-through optimization, not a state file. It never decides what to create or destroy; it only avoids re-asking a host that recently answered. The node remains the source of truth, and a cache miss simply re-gathers it live — so losing the cache loses speed, never correctness. This is the line that separates a cache from state: a corrupted or deleted state file can make a provisioning tool destroy the wrong resource, while a corrupted or deleted fact cache only forces a re-gather.

That is why "cache corruption" is not a category of worry here. The worst a stale or missing cache does is cost a round-trip or feed a slightly old value into a template — both recoverable by a fresh gather. The cache accelerates the stateless model without compromising it: the truth still lives on the node, and the cache is only a recent echo of what the node last said.

Common Mistakes
  • Reading hostvars['db1.larkspur.io'] from a web1-only play with no fact cache configured — db1 was never gathered this run, its facts are undefined, and the deploy fails on a host that's plainly up.
  • Setting a long fact_caching_timeout and then templating a host's cached IP after it changed addresses — the play renders yesterday's IP and points config at the wrong box, with no error to warn you.
  • Treating the fact cache as durable state and worrying about "corruption" — a stale or missing cache only costs a re-gather; it can never make Ansible create or destroy the wrong thing.
  • Pointing fact_caching_connection at a path the run can't write — a read-only or absent directory — so jsonfile caching silently no-ops and every run re-gathers anyway.
  • Sharing a Redis cache across environments without scoping, so staging's stale facts leak into a prod run's cross-host reads.
Best Practices
  • Turn on fact caching — jsonfile for a single control node, redis for shared CI runners — whenever targeted plays need to read non-targeted hosts' facts.
  • Set gathering = smart with caching so the fleet only re-gathers hosts whose cached facts are missing or expired, cutting run time on large inventories.
  • Tune fact_caching_timeout to the volatility of the facts you read — short where IPs and disks change often, longer for stable hardware facts.
  • Treat the cache as a speed optimization, not state — keep it per-environment, and rely on a cache miss re-gathering live so correctness never depends on the cache surviving.
  • Point fact_caching_connection at a path or server the run can actually write, and confirm writes land, so caching doesn't silently no-op into a full re-gather every run.
Comparable tools Puppet PuppetDB stores and serves node facts fleet-wide — the closest analog Salt mine caches cross-host data Chef chef-server holds node objects Redis · JSON file tree the concrete backends; no Terraform analog

Knowledge Check

Why can't a play that targets only web1 read db1's facts without caching?

  • Gathering is per play, so db1 was never gathered this run and its facts are undefined in hostvars
  • Cross-host reads stay disabled unless every host shares at least one inventory group with web1
  • hostvars only exposes facts for hosts in the same serial batch as the host running now
  • db1's facts live in a per-host state file that a narrowly targeted web1-only play refuses to open

What is the tradeoff between the jsonfile and redis cache backends?

  • jsonfile is simple and repo-local for one control node; redis is shared across control nodes and CI runners with a TTL
  • jsonfile is faster but loses everything on a control-node reboot, while redis is slower but stays durable on disk forever
  • jsonfile caches gathered facts while redis caches registered vars — the two serve different data
  • redis works only with gathering = explicit, while jsonfile requires the smart mode

With caching on and gathering = smart, which hosts does Ansible actually gather?

  • Only hosts whose cached facts are missing or expired — fresh-cache hosts skip the setup round-trip
  • Every host every run, since smart only changes where facts are stored, not whether they're gathered
  • Only hosts explicitly listed with gather_facts: true, ignoring the cache entirely
  • Only the first host of each group, then copies its facts to the rest

A host changed its IP and a deploy ran inside the cache TTL, rendering the old address. What does this tell you about the cache?

  • A stale cache costs correctness in the rendered value but never causes a wrong create or destroy — the worst case is an old value until the TTL lapses
  • The cache is corrupted by the IP change and must be fully rebuilt before any further deploy run is safe
  • The cache acted as a state file and actively decided to keep the host pinned at its old address
  • The deploy should have failed loudly the very moment the IP first drifted, and a silently rendered wrong value proves the caching layer is misconfigured at the protocol level

You got correct