Introduction

When Ansible's fact caching configuration changes the key format (such as switching from hostname-based keys to FQDN keys, or changing the cache prefix), previously cached entries become orphaned. These entries remain in the cache storage (Redis, JSON files, or memcached) but are inaccessible because the new key format doesn't match the old keys. Over time, this orphaned cache data consumes storage space and can cause confusing behavior where some hosts serve stale data while others appear unpopulated.

This issue commonly occurs after upgrading Ansible versions (which may change default key formats), modifying fact_caching_prefix in ansible.cfg, or changing inventory hostname conventions without clearing the cache.

Symptoms

Cache directory shows files that don't match expected key format:

```bash $ ls -la /tmp/ansible_facts/ total 512 drwxrwxr-x 2 ansible ansible 4096 Apr 12 08:26 . drwxrwxr-x 3 root root 4096 Apr 12 08:00 .. -rw-r--r-- 1 ansible ansible 15234 Apr 12 08:00 web-server-01 # Old format: short hostname -rw-r--r-- 1 ansible ansible 15234 Apr 12 08:00 db-server-01 # Old format -rw-r--r-- 1 ansible ansible 15234 Apr 12 08:15 web-server-01.example.com # New format: FQDN -rw-r--r-- 1 ansible ansible 15234 Apr 12 08:15 db-server-01.example.com # New format

# Both old and new format files exist, but Ansible only reads new format ```

Redis shows orphaned keys with old prefix:

```bash $ redis-cli keys "ansible_facts:*" 1) "ansible_facts:web-server-01" 2) "ansible_facts:db-server-01" 3) "ansible_facts:production_web-server-01_example_com" # New format

# Old keys are never accessed or cleaned ```

Playbook execution shows inconsistent cache behavior:

``` TASK [Gather facts] ************ ok: [web-server-01.example.com]

TASK [Debug cached fact] ************ ok: [web-server-01.example.com] => { "msg": "Variable 'ansible_facts.ansible_default_ipv4' is undefined" } # Fact should be cached but isn't found ```

Cache expiration doesn't remove old entries:

```bash $ ansible localhost -m meta -a "clear_facts=true" localhost | SUCCESS => { "ansible_facts": {} } # Only clears new-format keys

$ ls /tmp/ansible_facts/ | wc -l 2 # Old format files still present ```

Disk usage grows unexpectedly:

```bash $ du -sh /tmp/ansible_facts/ 1.2G /tmp/ansible_facts/

$ ls /tmp/ansible_facts/ | wc -l 5000 # Many orphaned entries from format changes ```

Common Causes

1. Ansible Version Upgrade Changes Default Key Format

Different Ansible versions use different key formats:

```yaml # Ansible 2.9: hostname only cache_key = "web-server-01"

# Ansible 2.15: includes inventory name cache_key = "production_web-server-01"

# After FQDN setting enabled cache_key = "web-server-01.example.com" ```

2. Custom Cache Prefix Change

Modifying the fact_caching_prefix creates keys that don't match old entries:

```yaml # ansible.cfg before [defaults] fact_caching_prefix = ansible_

# ansible.cfg after [defaults] fact_caching_prefix = tower_prod_

# Old keys: ansible_web-server-01 # New keys: tower_prod_web-server-01 ```

3. Inventory Hostname Convention Change

Changing from short hostnames to FQDNs in inventory:

```yaml # Old inventory [webservers] web-server-01

# New inventory [webservers] web-server-01.example.com

# Different keys for same host ```

4. Cache Plugin Change Without Migration

Switching cache plugins leaves old cache data behind:

```yaml # Before: JSON file cache fact_caching = jsonfile fact_caching_connection = /tmp/ansible_facts

# After: Redis cache fact_caching = redis fact_caching_connection = localhost:6379:0

# Old JSON files never cleaned up ```

5. Multi-Environment Key Collisions

Different environments using same cache without environment-specific prefixes:

```yaml # Production uses same Redis DB as staging fact_caching_connection = localhost:6379:0

# Keys collide: # staging: ansible_web-server-01 # production: ansible_web-server-01 # (same key, different data) ```

Step-by-Step Fix

Step 1: Identify Orphaned Cache Entries

Scan for cache entries that don't match current format:

```bash # For JSON file cache ls -la /tmp/ansible_facts/ | grep -v "$(date +%Y-%m-%d)"

# Check current cache key format ansible-config dump | grep -i fact_caching

# For Redis cache redis-cli keys "ansible_*" | while read key; do ttl=$(redis-cli ttl "$key") if [ "$ttl" -eq -1 ]; then echo "Orphaned key (no TTL): $key" fi done

# Check for keys matching old patterns redis-cli keys "ansible_facts:*" | grep -v "example.com$" ```

Create an inventory of all cache keys:

```bash # Generate list of expected cache keys from inventory ansible all --list-hosts | sed 's/^[ \t]*//' > /tmp/expected_hosts.txt

# List actual cache keys redis-cli keys "ansible_facts:*" | sed 's/ansible_facts://' > /tmp/actual_keys.txt

# Find orphaned keys comm -23 <(sort /tmp/actual_keys.txt) <(sort /tmp/expected_hosts.txt) > /tmp/orphaned_keys.txt echo "Orphaned keys: $(wc -l < /tmp/orphaned_keys.txt)" ```

Step 2: Clear Orphaned Cache Entries

Remove cache entries that don't match current format:

```bash # For JSON file cache - remove old format files find /tmp/ansible_facts -type f ! -name "*example.com*" -delete

# For Redis - remove keys matching old pattern redis-cli keys "ansible_facts:*" | grep -v "example.com$" | xargs -r redis-cli del

# Or clear all cache for fresh start ansible localhost -m meta -a "clear_facts=true" --become

# For Redis, flush the database redis-cli -n 0 FLUSHDB ```

Create a cleanup playbook:

```yaml # cleanup_cache.yml - name: Clean orphaned cache entries hosts: localhost vars: cache_type: "{{ lookup('env', 'ANSIBLE_CACHE_PLUGIN') | default('jsonfile') }}" cache_connection: "{{ lookup('env', 'ANSIBLE_CACHE_CONNECTION') | default('/tmp/ansible_facts') }}" inventory_hosts: "{{ groups['all'] | map('extract', hostvars, 'inventory_hostname') | list }}" valid_key_suffix: ".example.com"

tasks: - name: Get all cache entries (JSON file) find: paths: "{{ cache_connection }}" file_type: file register: cache_files when: cache_type == 'jsonfile'

  • name: Remove orphaned cache files
  • file:
  • path: "{{ item.path }}"
  • state: absent
  • loop: "{{ cache_files.files | default([]) }}"
  • when:
  • - cache_type == 'jsonfile'
  • - valid_key_suffix not in item.path
  • - item.path | basename not in inventory_hosts
  • register: removed_files
  • name: Report cleanup results
  • debug:
  • msg: "Removed {{ removed_files.results | selectattr('changed') | list | length }} orphaned cache files"
  • when: cache_type == 'jsonfile'
  • name: Get all Redis keys
  • command: redis-cli keys "ansible_facts:*"
  • register: redis_keys
  • changed_when: false
  • when: cache_type == 'redis'
  • name: Remove orphaned Redis keys
  • command: redis-cli del {{ item }}
  • loop: "{{ redis_keys.stdout_lines | default([]) }}"
  • when:
  • - cache_type == 'redis'
  • - valid_key_suffix not in item
  • - item | replace('ansible_facts:', '') not in inventory_hosts
  • register: removed_keys
  • name: Report Redis cleanup
  • debug:
  • msg: "Removed {{ removed_keys.results | selectattr('changed') | list | length }} orphaned Redis keys"
  • when: cache_type == 'redis'
  • `

Step 3: Implement Consistent Cache Key Format

Configure Ansible to use a consistent, documented key format:

```yaml # ansible.cfg [defaults] # Enable fact caching gathering = smart fact_caching = jsonfile fact_caching_connection = /var/cache/ansible/facts fact_caching_timeout = 86400

# Use explicit cache key format fact_caching_prefix = prod_ # Keys will be: prod_hostname.example.com

# For Redis cache # fact_caching = redis # fact_caching_connection = localhost:6379:1

# Enable fact caching for all plays cache_plugin = jsonfile cache_plugin_connection = /var/cache/ansible/facts ```

Configure environment-specific prefixes:

```yaml # In inventory group_vars # group_vars/production.yml fact_cache_prefix: "prod_" fact_cache_db: 1

# group_vars/staging.yml fact_cache_prefix: "staging_" fact_cache_db: 2

# In ansible.cfg fact_caching_prefix = {{ fact_cache_prefix | default('default_') }} ```

Step 4: Create Cache Key Migration Script

Migrate existing cache entries to new format:

```python #!/usr/bin/env python3 # migrate_cache_keys.py

import os import json import redis import shutil from pathlib import Path

def migrate_json_cache(old_dir, new_dir, key_transform): """Migrate JSON cache files with new key format.""" old_path = Path(old_dir) new_path = Path(new_dir)

new_path.mkdir(parents=True, exist_ok=True)

migrated = 0 for old_file in old_path.glob('*'): if old_file.is_file(): old_key = old_file.name new_key = key_transform(old_key)

if new_key and new_key != old_key: # Read old cache entry with open(old_file, 'r') as f: data = json.load(f)

# Write to new location new_file = new_path / new_key with open(new_file, 'w') as f: json.dump(data, f)

migrated += 1 print(f"Migrated: {old_key} -> {new_key}")

return migrated

def migrate_redis_cache(redis_host, redis_port, db, old_prefix, new_prefix): """Migrate Redis cache keys with new prefix.""" r = redis.Redis(host=redis_host, port=redis_port, db=db)

migrated = 0 cursor = 0 while True: cursor, keys = r.scan(cursor=cursor, match=f"{old_prefix}*") for key in keys: key_str = key.decode('utf-8') new_key = key_str.replace(old_prefix, new_prefix, 1)

if new_key != key_str: # Get old value value = r.get(key) if value: # Set new key r.set(new_key, value) # Copy TTL ttl = r.ttl(key) if ttl > 0: r.expire(new_key, ttl) # Delete old key r.delete(key) migrated += 1 print(f"Migrated: {key_str} -> {new_key}")

if cursor == 0: break

return migrated

def add_fqdn_suffix(hostname, domain="example.com"): """Transform short hostname to FQDN.""" if '.' not in hostname: return f"{hostname}.{domain}" return hostname

if __name__ == "__main__": import argparse

parser = argparse.ArgumentParser(description='Migrate Ansible cache keys') parser.add_argument('--type', choices=['json', 'redis'], required=True) parser.add_argument('--old-dir', default='/tmp/ansible_facts') parser.add_argument('--new-dir', default='/var/cache/ansible/facts') parser.add_argument('--redis-host', default='localhost') parser.add_argument('--redis-port', type=int, default=6379) parser.add_argument('--redis-db', type=int, default=0) parser.add_argument('--old-prefix', default='ansible_') parser.add_argument('--new-prefix', default='prod_')

args = parser.parse_args()

if args.type == 'json': migrated = migrate_json_cache( args.old_dir, args.new_dir, lambda k: add_fqdn_suffix(k) ) print(f"\nMigrated {migrated} JSON cache files") else: migrated = migrate_redis_cache( args.redis_host, args.redis_port, args.redis_db, args.old_prefix, args.new_prefix ) print(f"\nMigrated {migrated} Redis keys") ```

Step 5: Add Cache Validation to Playbooks

Include cache validation in playbooks to detect format mismatches:

```yaml # validate_cache.yml - name: Validate cache key format hosts: localhost vars: expected_suffix: ".example.com" cache_connection: "{{ lookup('config', 'FACT_CACHING_CONNECTION') | default('/tmp/ansible_facts') }}"

tasks: - name: Check cache directory for format mismatches find: paths: "{{ cache_connection }}" file_type: file excludes: "*{{ expected_suffix }}" register: mismatched_files

  • name: Warn about cache format mismatches
  • debug:
  • msg: |
  • WARNING: Found {{ mismatched_files.matched }} cache files with unexpected format.
  • These entries may be orphaned and should be cleaned.
  • Run: ansible localhost -m meta -a 'clear_facts=true'
  • when: mismatched_files.matched > 0
  • name: Verify cache entries for inventory hosts
  • stat:
  • path: "{{ cache_connection }}/{{ item }}{{ expected_suffix }}"
  • register: cache_stat
  • loop: "{{ groups['all'] }}"
  • delegate_to: localhost
  • name: Report missing cache entries
  • debug:
  • msg: "No cache entry for {{ item.item }}"
  • when: not item.stat.exists
  • loop: "{{ cache_stat.results }}"
  • `

Verification

Test that cache keys are consistent:

```bash # Clear cache completely ansible localhost -m meta -a "clear_facts=true"

# Gather facts for all hosts ansible all -m setup

# Verify cache key format ls /var/cache/ansible/facts/ # Should show only keys matching expected format

# Check Redis keys match expected format redis-cli keys "prod_*" | head -10 # All should match prod_hostname.example.com

# Test cache retrieval ansible web-server-01.example.com -m debug -a "msg={{ ansible_facts.ansible_hostname }}" # Should return cached value without re-gathering facts ```

Verify cleanup removed all orphaned entries:

```bash # Count cache entries ls /var/cache/ansible/facts/ | wc -l

# Count inventory hosts ansible all --list-hosts | wc -l

# Should match (or cache entries should be close, accounting for removed hosts) ```

  • [ansible-cache-serves-old-data-after-deployment](/articles/ansible-cache-serves-old-data-after-deployment) - Stale cache data issues
  • [ansible-artifact-download-uses-an-old-mirror-after-proxy-change](/articles/ansible-artifact-download-uses-an-old-mirror-after-proxy-change) - Stale configuration references
  • [ansible-environment-variable-prefix-change-leaves-workers-on-legacy-settings](/articles/ansible-environment-variable-prefix-change-leaves-workers-on-legacy-settings) - Configuration prefix issues
  • [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 Key Format Change Leaves O", "description": "Learn how to fix Ansible Cache Key Format Change Leaves Old Entries Undrainable. 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-key-format-change-leaves-old-entries-undrainable", "publisher": { "@type": "Organization", "name": "FixWikiHub", "url": "https://www.fixwikihub.com" }, "author": { "@type": "Person", "name": "FixWikiHub Editorial Team" }, "datePublished": "2026-03-07T09:33:31.349Z", "dateModified": "2026-03-07T09:33:31.349Z" } </script>