Introduction

After deploying configuration changes, new packages, or infrastructure updates via Ansible, subsequent playbook runs continue to use outdated fact data. The cached facts don't reflect the current state of managed hosts, leading to playbooks making decisions based on stale information. This occurs when fact caching is enabled but cache entries aren't invalidated after changes that would alter host facts.

The problem manifests in production when a deployment changes IP addresses, modifies disk layouts, updates package inventories, or alters system configuration, but Ansible playbooks continue using the old cached values. This can cause incorrect conditional logic, wrong template renders, and deployment failures.

Symptoms

Playbook uses outdated IP address from cached facts:

bash
TASK [Configure firewall for new IP] *******************************************
fatal: [web-server-01]: FAILED! => {
    "msg": "iptables: No chain/target/match by that name."
}
# The IP changed from 192.168.1.100 to 192.168.1.200
# But Ansible still has cached fact: ansible_default_ipv4.address = 192.168.1.100

Template renders with stale package version:

```yaml # Template using cached fact - name: Render config template: src: app.conf.j2 dest: /etc/app/app.conf # Uses ansible_facts.packages.nginx[0].version from cache

# Config shows old version after package upgrade $ cat /etc/app/app.conf nginx_version=1.18.0 # Should be 1.24.0 after upgrade ```

Debug shows cached facts don't match current reality:

```bash $ ansible web-server-01 -m debug -a "msg={{ ansible_default_ipv4.address }}" web-server-01 | SUCCESS => { "msg": "192.168.1.100" # Cached value }

$ ssh web-server-01 "ip addr show eth0 | grep inet" inet 192.168.1.200/24 brd 192.168.1.255 scope global dynamic eth0 # Actual current IP ```

Fact cache files show old timestamps:

```bash $ ls -la /var/cache/ansible/facts/ total 8 -rw-r--r-- 1 ansible ansible 15234 Mar 15 10:22 web-server-01 # Cache is 30 days old, but fact_caching_timeout is 86400 (24 hours)

$ stat /var/cache/ansible/facts/web-server-01 Modify: 2026-03-15 10:22:15 # File wasn't updated despite host changes ```

Distributed cache inconsistency in Tower cluster:

```bash # Tower node 1 $ redis-cli get "ansible_facts:web-server-01" "{\"ansible_default_ipv4\": {\"address\": \"192.168.1.100\"}}"

# Tower node 2 (after deployment) $ redis-cli get "ansible_facts:web-server-01" "{\"ansible_default_ipv4\": {\"address\": \"192.168.1.200\"}}"

# Different cached values across nodes ```

Common Causes

1. Fact Caching Timeout Too Long

Cache timeout is set longer than typical change intervals:

yaml
# ansible.cfg
[defaults]
fact_caching_timeout = 604800  # 7 days - too long for dynamic environments

2. Smart Gathering Mode Doesn't Refresh Changed Hosts

The smart gathering mode only skips fact collection if cache exists, regardless of whether facts have changed:

yaml
[defaults]
gathering = smart
# Only checks cache existence, not validity

3. Redis Replication Lag

In a Tower cluster with Redis replication, facts may be served from a replica that hasn't synced:

bash
Primary: ansible_facts:web-server-01 = "192.168.1.200" (updated)
Replica: ansible_facts:web-server-01 = "192.168.1.100" (stale, not synced yet)

4. Fact Cache Not Invalidated on Host Changes

No explicit cache invalidation after infrastructure changes:

```yaml - name: Change IP address command: ip addr add 192.168.1.200/24 dev eth0

# No cache invalidation here # Next playbook still sees old IP in cache

  • name: Use new IP in firewall
  • iptables:
  • source: "{{ ansible_default_ipv4.address }}"
  • # Uses cached old IP
  • `

5. JSON Cache Files with Extended Attributes

File-based cache entries with future timestamps due to clock skew:

bash
$ stat /var/cache/ansible/facts/web-server-01
  Modify: 2026-12-25 10:00:00  # Future date due to clock sync issue

Step-by-Step Fix

Step 1: Identify Stale Cache Entries

Check cache freshness against actual host state:

```bash # Get cache file timestamp CACHE_TIME=$(stat -c %Y /var/cache/ansible/facts/web-server-01)

# Get host's reported time HOST_TIME=$(ssh web-server-01 "date +%s")

# Calculate age AGE=$((HOST_TIME - CACHE_TIME)) echo "Cache age: $AGE seconds"

# Get cache timeout setting grep fact_caching_timeout ansible.cfg ```

Compare cached facts to live facts:

```yaml # check_cache_freshness.yml - name: Compare cached vs live facts hosts: all gather_facts: false vars: cache_dir: /var/cache/ansible/facts

tasks: - name: Read cached facts set_fact: cached_facts: "{{ lookup('file', cache_dir + '/' + inventory_hostname) | from_json }}" delegate_to: localhost ignore_errors: yes

  • name: Gather fresh facts
  • setup:
  • register: fresh_facts
  • name: Compare IP addresses
  • debug:
  • msg: |
  • Cached IP: {{ cached_facts.ansible_default_ipv4.address | default('N/A') }}
  • Fresh IP: {{ fresh_facts.ansible_facts.ansible_default_ipv4.address }}
  • Match: {{ cached_facts.ansible_default_ipv4.address | default('') == fresh_facts.ansible_facts.ansible_default_ipv4.address }}
  • when: cached_facts is defined
  • name: Flag mismatched facts
  • set_fact:
  • cache_mismatch: true
  • when:
  • - cached_facts is defined
  • - cached_facts.ansible_default_ipv4.address | default('') != fresh_facts.ansible_facts.ansible_default_ipv4.address
  • name: Report stale caches
  • debug:
  • msg: "WARNING: Cached facts are stale for {{ inventory_hostname }}"
  • when: cache_mismatch | default(false)
  • `

Step 2: Clear Cache After Infrastructure Changes

Add explicit cache invalidation to deployment playbooks:

```yaml # deploy_with_cache_clear.yml - name: Deploy with proper cache handling hosts: web_servers vars: clear_cache_before: true clear_cache_after: true

tasks: - name: Clear fact cache before changes meta: clear_facts when: clear_cache_before

  • name: Change IP configuration
  • command: nmcli con mod eth0 ipv4.addresses 192.168.1.200/24
  • notify: Reload network
  • name: Apply new configuration
  • command: nmcli con up eth0
  • notify: Clear cache after network change
  • name: Verify new IP is active
  • command: ip -4 addr show eth0
  • register: ip_output
  • changed_when: false
  • name: Extract new IP
  • set_fact:
  • new_ip: "{{ ip_output.stdout | regex_search('inet\\s+([\\d.]+)', '\\1') | first }}"
  • name: Clear cache after infrastructure change
  • meta: clear_facts
  • when: clear_cache_after
  • name: Gather fresh facts
  • setup:
  • name: Verify cached fact matches reality
  • assert:
  • that:
  • - ansible_default_ipv4.address == new_ip
  • fail_msg: "Cached fact {{ ansible_default_ipv4.address }} doesn't match actual IP {{ new_ip }}"
  • success_msg: "Fact cache updated correctly"

handlers: - name: Reload network systemd: name: NetworkManager state: reloaded

  • name: Clear cache after network change
  • meta: clear_facts
  • `

Step 3: Configure Appropriate Cache Timeouts

Set cache timeout based on infrastructure volatility:

```yaml # ansible.cfg for dynamic environments [defaults] gathering = smart fact_caching = jsonfile fact_caching_connection = /var/cache/ansible/facts fact_caching_timeout = 3600 # 1 hour for dynamic environments

# For more static environments # fact_caching_timeout = 86400 # 24 hours ```

Use different timeouts for different fact types:

```yaml # playbook.yml with selective cache control - name: Selective fact cache management hosts: all vars: # Critical facts should be fresh critical_fact_timeout: 300 # 5 minutes # Less volatile facts can be cached longer stable_fact_timeout: 86400 # 24 hours

tasks: - name: Check cache age for critical facts stat: path: "/var/cache/ansible/facts/{{ inventory_hostname }}" register: cache_stat delegate_to: localhost

  • name: Force refresh if cache too old
  • block:
  • - name: Clear old facts
  • meta: clear_facts
  • name: Gather fresh facts
  • setup:
  • when:
  • - cache_stat.stat.exists
  • - ansible_date_time.epoch | int - cache_stat.stat.mtime > critical_fact_timeout
  • name: Use cached facts if fresh enough
  • setup:
  • when: not cache_stat.stat.exists or ansible_date_time.epoch | int - cache_stat.stat.mtime <= critical_fact_timeout
  • `

Step 4: Implement Cache Invalidation Triggers

Create a cache invalidation callback plugin:

```python # callback_plugins/cache_invalidator.py

from ansible.plugins.callback import CallbackBase from ansible.utils.cache import cache import os

class CallbackModule(CallbackBase): CALLBACK_VERSION = 2.0 CALLBACK_TYPE = 'notification' CALLBACK_NAME = 'cache_invalidator' CALLBACK_NEEDS_WHITELIST = False

# Tasks that should invalidate cache INVALIDATING_MODULES = [ 'command', 'shell', 'systemd', 'service', 'apt', 'yum', 'dnf', 'package', 'nmcli', 'interfaces_file', 'hostname', 'user', 'group', 'mount', 'filesystem' ]

def v2_runner_on_ok(self, result): task = result._task host = result._host.name

# Check if this task might have changed system state if task.action in self.INVALIDATING_MODULES: if result._result.get('changed', False): self._invalidate_host_cache(host)

def _invalidate_host_cache(self, host): try: cache_key = f"ansible_facts_{host}" if cache.has(cache_key): cache.delete(cache_key) self._display.v(f"Invalidated cache for {host} due to system changes") except Exception as e: self._display.warning(f"Failed to invalidate cache: {e}") ```

Step 5: Fix Distributed Cache Consistency

For Redis-backed cache in Tower clusters:

```yaml # Configure Redis for immediate consistency # /etc/redis/redis.conf appendonly yes appendfsync always replica-serve-stale-data no # Don't serve stale data during sync

# Ensure all Tower nodes use same Redis instance # Tower settings BROKER_URL = 'redis://redis-primary:6379/0' CACHE_BACKEND = 'redis://redis-primary:6379/1' ```

Add cache versioning for cluster consistency:

```yaml # ansible.cfg [defaults] fact_caching = redis fact_caching_connection = redis://redis-primary:6379/1 fact_caching_prefix = "v2_prod_" # Increment version when schema changes

# Before major deployment, increment version to force fresh cache ```

Create a cache warmup playbook:

```yaml # warmup_cache.yml - name: Warm up fact cache across all Tower nodes hosts: localhost vars: tower_nodes: - tower-node-1 - tower-node-2 - tower-node-3

tasks: - name: Clear cache on all nodes command: redis-cli -h {{ item }} -n 1 FLUSHDB loop: "{{ tower_nodes }}"

  • name: Trigger fact gathering for all managed hosts
  • uri:
  • url: "https://{{ item }}/api/v2/inventory_sources/1/update/"
  • method: POST
  • user: "{{ tower_user }}"
  • password: "{{ tower_password }}"
  • status_code: 202
  • loop: "{{ tower_nodes }}"
  • name: Wait for cache population
  • pause:
  • minutes: 5
  • name: Verify cache consistency
  • command: redis-cli -h {{ item }} -n 1 DBSIZE
  • register: cache_size
  • loop: "{{ tower_nodes }}"
  • name: Compare cache sizes
  • debug:
  • msg: "Node {{ item.item }} has {{ item.stdout }} cache entries"
  • loop: "{{ cache_size.results }}"
  • `

Verification

Test cache invalidation works correctly:

```bash # Get current cached IP ansible web-server-01 -m debug -a "msg={{ ansible_default_ipv4.address }}"

# Change IP on host ssh web-server-01 "sudo ip addr add 10.0.0.100/24 dev eth0"

# Clear cache ansible web-server-01 -m meta -a "clear_facts=true"

# Gather fresh facts ansible web-server-01 -m setup --tree /tmp/fresh_facts

# Verify new IP in fresh facts cat /tmp/fresh_facts/web-server-01 | jq '.ansible_facts.ansible_default_ipv4.address' ```

Verify cache timeout is respected:

```bash # Set short timeout for testing export ANSIBLE_CACHE_PLUGIN_TIMEOUT=60

# Gather facts ansible all -m setup

# Wait 61 seconds sleep 61

# Check if cache was refreshed ansible all -m setup -v # Should show "gathering facts" not "using cached facts" ```

Check distributed cache consistency:

```bash # On each Tower node for node in tower-{1,2,3}; do echo "=== $node ===" ssh $node "redis-cli -n 1 keys 'ansible_facts:*' | wc -l" done

# All nodes should report same count ```

  • [ansible-cache-key-format-change-leaves-old-entries-undrainable](/articles/ansible-cache-key-format-change-leaves-old-entries-undrainable) - Orphaned cache entries
  • [ansible-artifact-download-uses-an-old-mirror-after-proxy-change](/articles/ansible-artifact-download-uses-an-old-mirror-after-proxy-change) - Stale configuration
  • [ansible-configuration-change-does-not-apply-until-full-restart](/articles/ansible-configuration-change-does-not-apply-until-full-restart) - Configuration persistence
  • [WordPress troubleshooting: Ansible Artifact Download Uses an Old Mi](ansible-artifact-download-uses-an-old-mirror-after-proxy-change)
  • [WordPress troubleshooting: Ansible Audit Trail Misses Events Under ](ansible-audit-trail-misses-events-under-burst-load)
  • [WordPress troubleshooting: Ansible Background Worker Gets Stuck in ](ansible-background-worker-stuck-in-a-retry-loop)
  • [WordPress troubleshooting: Ansible Backup Completes but Restore Fai](ansible-backup-completes-but-restore-fails-checksum-validation)
  • [WordPress troubleshooting: Ansible Batch Importer Duplicates Rows A](ansible-batch-importer-duplicates-rows-after-a-retry)

<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "TechArticle", "headline": "WordPress troubleshooting: Ansible Cache Serves Old Data After Depl", "description": "Learn how to fix Ansible Cache Serves Old Data After Deployment. Professional WordPress troubleshooting solutions with step-by-step guidance. WP error fix, WordPress optimization, WP security, WordPress performance.", "url": "https://www.fixwikihub.com/ansible-cache-serves-old-data-after-deployment", "publisher": { "@type": "Organization", "name": "FixWikiHub", "url": "https://www.fixwikihub.com" }, "author": { "@type": "Person", "name": "FixWikiHub Editorial Team" }, "datePublished": "2026-02-12T07:36:41.203Z", "dateModified": "2026-02-12T07:36:41.203Z" } </script>