Skip to main content
RunBook Academy

AnsibleXXIX · Dynamic InventoryOperating a dynamic source

Inventory caching and staleness

Advanced⏱ ~24 minbash

What you'll learn

  • Configure inventory caching and name the four options that control it
  • Predict which run sees a change made at the provider and which does not
  • Use --flush-cache deliberately rather than as a reflex
  • State the distinct operational risk of a stale inventory and of a fresh one

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.

An inventory plugin makes an API call. API calls cost time, cost money on some providers, and are rate limited on all of them. Ansible’s answer is a cache: keep the last answer, reuse it until it expires.

That is a sensible engineering decision and it introduces a genuinely new operational object into your estate — a host list that is a picture of the fleet at some point in the past, and whose age you cannot see in the output.

Both directions of that are dangerous, and they are dangerous in different ways, which is the point this lesson is built around.

The four options, and where they come from

Every inventory plugin that supports caching gets the same options, because they come from a single documentation fragment that plugins include rather than each defining their own. That is why the AWS plugin, the Azure plugin and a plugin you write yourself all spell it the same way.

OptionDefaultWhat it does
cachefalseMaster switch. Off unless you turn it on.
cache_pluginmemoryWhich cache backend to use.
cache_timeout3600Cache lifetime in seconds.
cache_connection(none)Where the cache lives — a path, a connection string.
cache_prefixansible_inventory_Prefix for the cache files or table names.

No core inventory plugin includes that fragment — constructed, yaml and ini read local files and have nothing to cache — so the place to read the authoritative defaults is the fragment source that ships with ansible-core:

Read-only / Safethe shared doc fragment, from the installed package
$ sed -n '12,48p' $(python3 -c 'import ansible,os;print(os.path.dirname(ansible.__file__))')/plugins/doc_fragments/inventory_cache.py
options:
cache:
  description:
    - Toggle to enable/disable the caching of the inventory's source data, requires a cache plugin setup to work.
  type: bool
  default: no
  env:
    - name: ANSIBLE_INVENTORY_CACHE
  ini:
    - section: inventory
      key: cache
cache_plugin:
  description:
    - Cache plugin to use for the inventory's source data.
  type: str
  default: memory
  env:
    - name: ANSIBLE_CACHE_PLUGIN
    - name: ANSIBLE_INVENTORY_CACHE_PLUGIN
  ini:
    - section: defaults
      key: fact_caching
    - section: inventory
      key: cache_plugin
cache_timeout:
  description:
    - Cache duration in seconds.
  default: 3600
  type: int
  env:
    - name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
    - name: ANSIBLE_INVENTORY_CACHE_TIMEOUT
  ini:
    - section: defaults
      key: fact_caching_timeout
    - section: inventory
      key: cache_timeout

Two details in that output are worth noticing now, because they cause confusion later. Each option has two environment variables and two ansible.cfg keys, and one of each pair is the fact-caching name — ANSIBLE_CACHE_PLUGIN, [defaults] fact_caching. Setting those affects inventory caching and fact caching together. The ANSIBLE_INVENTORY_CACHE_* and [inventory] forms affect inventory caching only, and are the ones to prefer when you mean one and not the other.

memory is not a cache in the useful sense

The default cache_plugin is memory, and memory means this process. It disappears when the command exits. It is genuinely useful — a single playbook run that references the inventory several times hits the API once — but between two ansible-playbook invocations it does nothing at all.

Only two cache plugins ship in core:

Read-only / Safecache plugins in core
$ ansible-doc -t cache -l
ansible.builtin.jsonfile JSON formatted files
ansible.builtin.memory   RAM backed, non persistent

So a cache that survives between runs means jsonfile unless you have installed a collection. jsonfile writes one file per source into the directory given by cache_connection.

Read-only / Safea source with a persistent cache
plugin: example.provider.source
cache: true
cache_plugin: jsonfile
cache_connection: /var/cache/ansible/inventory
cache_timeout: 900

What a cache hit looks like from outside

Nothing. That is the entire problem.

Here is a source whose backing data gains a host between two runs, with cache: true and a jsonfile cache:

Read-only / Saferun one - populates the cache
$ ansible-inventory -i demo.yml --graph
@all:
|--@ungrouped:
|--@web:
|  |--web01.example.com
|  |--web02.example.com
|--@db:
|  |--db01.example.com

The source now contains a fourth host, web03.example.com. Run it again, unchanged:

Read-only / Saferun two - the source has changed, the answer has not
$ ansible-inventory -i demo.yml --graph
@all:
|--@ungrouped:
|--@web:
|  |--web01.example.com
|  |--web02.example.com
|--@db:
|  |--db01.example.com
Read-only / Saferun three - with --flush-cache
$ ansible-inventory -i demo.yml --graph --flush-cache
@all:
|--@ungrouped:
|--@web:
|  |--web01.example.com
|  |--web02.example.com
|  |--web03.example.com
|--@db:
|  |--db01.example.com

Read the two graphs again. They are the same command producing different answers, and there is nothing in the first one that tells you it is old. An inventory cache has no visible timestamp in normal output. The only way to know how old a host list is, is to know how the cache is configured.

Both directions are real, and they are not symmetrical

The instinct is “stale is bad, fresh is good”. That is half right, and the half that is wrong is the half that causes outages.

Stale: you miss hosts, and you target ghosts

A cached list that predates a scale-up misses the new instances. A security patch run reports complete success across every host it knew about, and three machines that came up forty minutes ago are unpatched. The recap is green. Nothing indicates a gap.

A cached list that predates a scale-down contains machines that no longer exist. Those show as unreachable, which is at least loud — but a run configured to tolerate unreachable hosts absorbs them silently, and now “3 unreachable” is a number people have learned to ignore.

Fresh: your blast radius changes without a diff

Now the other direction, which people do not expect.

A run with --flush-cache sees the fleet as it is right now, which includes anything created in the last five minutes by anybody. If your grouping is opt-out — a host is managed unless tagged otherwise — then a freshly queried inventory is precisely how a machine nobody told you about ends up in a play.

Staleness gives you a small, bounded window of not knowing. Freshness gives you an unbounded set of hosts you have never reviewed. Neither is safe on its own; what makes either safe is knowing which one you are getting and verifying the host list before the run.

Flushing, and the flag that is easy to misread

--flush-cache exists on both ansible-playbook and ansible-inventory, and it does two related things:

  1. It discards the existing cache for the inventory sources in play, so the plugin re-queries the provider.
  2. It repopulates the cache with the new answer, so the next run without the flag sees the fresher list rather than the old one.
  3. On ansible-playbook it also clears the fact cache for the hosts in the run, which is a separate cache with a separate purpose - if you are using fact caching to avoid re-gathering, this flag throws that away too.

Point three is the one that surprises people, and it is why --flush-cache should not be pinned into every scheduled command “to be safe”. A nightly job that flushes on every run re-queries the provider API and re-gathers facts across the whole fleet, which is a large amount of work to buy a freshness guarantee you could have got by setting cache_timeout sensibly.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A team enables cache: true on their cloud inventory source and changes nothing else. How stale can the host list be?

  2. Q2. A run served from an inventory cache looks identical in its output to one that queried the provider.

  3. Q3. Why is it a bad habit to add --flush-cache to every scheduled ansible-playbook invocation?

  4. Q4. A patching job and a reporting job share one jsonfile cache directory. Which problems can that cause? Select all that apply.

Passing score: 75%. Answers are checked in this browser.