Introduction
Running Ansible against 10 hosts is easy. Running it against 1,000+ hosts requires deliberate performance tuning. This guide covers every lever: connection optimization, parallelism, fact caching, pull mode, execution environments, and architecture patterns that let Ansible scale to enterprise fleet sizes.
Baseline Performance Settings
# ansible.cfg — optimized for large fleets
[defaults]
forks = 50
timeout = 30
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 86400
stdout_callback = default
callback_whitelist = timer, profile_tasks
interpreter_python = auto_silent
host_key_checking = False
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o PreferServerAlive=yes -o ServerAliveInterval=15
control_path_dir = /tmp/.ansible-cp
Key Settings Explained
| Setting | Default | Recommended | Impact |
|---|---|---|---|
forks | 5 | 50-100 | Parallel host connections |
pipelining | False | True | Reduces SSH operations by ~2x |
gathering | implicit | smart | Cache facts, skip if cached |
fact_caching | memory | jsonfile/redis | Persist facts across runs |
ControlPersist | 60s | 60s-300s | Reuse SSH connections |
Optimize Parallelism
Forks Tuning
# Scale forks based on controller resources
# Rule of thumb: 1 fork per 100MB RAM, max ~200
# 16GB RAM → forks = 100
# 32GB RAM → forks = 200
[defaults]
forks = 100
# Override per-run
ansible-playbook site.yml -f 200
# Or per-play
# In playbook — override forks for specific plays
- name: Quick check across all hosts
hosts: all
strategy: free
tasks:
- name: Ping all hosts
ansible.builtin.ping:
Strategy Plugins
# Linear (default) — waits for all hosts per task
- hosts: all
strategy: linear
# Free — each host runs independently (fastest)
- hosts: all
strategy: free
# Host pinned — reduces memory for large inventories
- hosts: all
strategy: host_pinned
# ansible.cfg — use free strategy globally
[defaults]
strategy = free
Fact Caching with Redis
# Deploy Redis for fact caching
- name: Deploy fact cache
hosts: ansible_controllers
become: true
tasks:
- name: Install Redis
ansible.builtin.package:
name: redis-server
state: present
- name: Start Redis
ansible.builtin.service:
name: redis-server
state: started
enabled: true
- name: Configure ansible.cfg for Redis caching
community.general.ini_file:
path: /etc/ansible/ansible.cfg
section: defaults
option: "{{ item.key }}"
value: "{{ item.value }}"
loop:
- { key: fact_caching, value: redis }
- { key: fact_caching_connection, value: "localhost:6379:0" }
- { key: fact_caching_timeout, value: "86400" }
# ansible.cfg — Redis fact caching
[defaults]
fact_caching = redis
fact_caching_connection = localhost:6379:0
fact_caching_timeout = 86400
# Install: pip install redis
Reduce Gather Facts Overhead
# Only gather what you need
- hosts: all
gather_facts: false
tasks:
- name: Gather only network facts
ansible.builtin.setup:
gather_subset:
- network
when: need_network_info | default(false)
- name: Skip facts entirely for simple tasks
ansible.builtin.command: uptime
# Or use gather_subset globally
- hosts: all
gather_facts: true
module_defaults:
ansible.builtin.setup:
gather_subset:
- "!all"
- "!min"
- network
- hardware
SSH Connection Optimization
# ansible.cfg — aggressive SSH optimization
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=300s -o PreferServerAlive=yes -o ServerAliveInterval=15 -o Compression=no
control_path_dir = /tmp/.ansible-cp
transfer_method = piped
retries = 3
Mitogen (Alternative SSH Transport)
# Install Mitogen for 2-7x speed improvement
pip install mitogen
# Configure
# ansible.cfg
# [defaults]
# strategy_plugins = /path/to/mitogen/ansible_mitogen/plugins/strategy
# strategy = mitogen_linear
Pull Mode for Massive Scale
# Deploy ansible-pull for 1000+ nodes
- name: Configure ansible-pull
hosts: all
become: true
tasks:
- name: Install Ansible on targets
ansible.builtin.pip:
name: ansible-core
state: present
- name: Deploy ansible-pull cron job
ansible.builtin.cron:
name: "Ansible pull"
minute: "{{ 59 | random(seed=inventory_hostname) }}"
hour: "*"
job: >
ansible-pull
-U {{ playbook_repo }}
-i {{ inventory_file }}
-d /opt/ansible-pull
--clean
--only-if-changed
>> /var/log/ansible-pull.log 2>&1
- name: Deploy ansible-pull inventory
ansible.builtin.template:
src: pull-inventory.yml.j2
dest: /etc/ansible/pull-inventory.yml
When to Use Pull vs Push
| Criteria | Push (default) | Pull (ansible-pull) |
|---|---|---|
| Hosts | < 500 | 500+ |
| Network | Good connectivity | Intermittent/NAT |
| Control | Centralized | Distributed |
| Latency | Low | Higher (cron-based) |
| Complexity | Simple | More setup |
Batch and Serial Execution
# Roll out changes in batches
- hosts: web_servers
serial: "20%"
max_fail_percentage: 10
tasks:
- name: Deploy application
ansible.builtin.include_role:
name: deploy
# Or use absolute numbers
- hosts: all
serial:
- 1 # Canary
- 5 # Small batch
- "25%" # Quarter
- "100%" # Everything else
Monitor Execution Performance
# ansible.cfg — enable profiling
[defaults]
callbacks_enabled = timer, profile_tasks, profile_roles
# Output shows time per task:
# TASK [Install packages] ****
# ok: [host1] => {"changed": false}
# Wednesday 13 April 2026 10:00:00 +0000 (0:00:45.123)
# Custom callback for logging to file
- hosts: all
tasks:
- name: Record execution metrics
ansible.builtin.set_stats:
data:
execution_time: "{{ ansible_date_time.epoch }}"
hosts_processed: "{{ ansible_play_hosts | length }}"
Execution Environments (EE)
# execution-environment.yml
version: 3
dependencies:
galaxy: requirements.yml
python: requirements.txt
system: bindep.txt
build_arg_defaults:
ANSIBLE_GALAXY_CLI_COLLECTION_OPTS: "--pre"
images:
base_image: quay.io/ansible/ansible-runner:latest
# Build and use
ansible-builder build --tag my-ee:latest
ansible-navigator run site.yml --eei my-ee:latest
Architecture for 1,000+ Nodes
┌──────────────────────────────┐
│ AAP Controller / AWX │
│ (Web UI + API + scheduling) │
└──────────┬───────────────────┘
│
┌──────┴──────┐
▼ ▼
┌────────┐ ┌────────┐
│ Hop 1 │ │ Hop 2 │ ← Execution nodes
│ 500 │ │ 500 │ (spread load)
│ hosts │ │ hosts │
└────────┘ └────────┘
Redis ─── Fact cache (shared)
Git ─── Playbook source of truth
Related Articles
Conclusion
Scaling Ansible to 1,000+ nodes requires tuning at every layer: increase forks to 50-100, enable SSH pipelining and ControlPersist, cache facts in Redis, use strategy: free for independent tasks, gather only the facts you need, and consider ansible-pull for massive fleets. For enterprise scale, AAP/AWX adds execution nodes that distribute the load. The key insight: Ansible's default settings are optimized for correctness and safety, not speed — tuning them for your environment can deliver 5-10x performance improvements with no playbook changes.