Introduction
Ansible playbooks can be slow when managing hundreds of hosts. The good news: a few configuration changes can cut execution time by 50-75%. This guide covers every optimization technique from quick wins to advanced strategies.
Quick Wins (ansible.cfg)
# ansible.cfg — performance settings
[defaults]
# Run on more hosts in parallel (default: 5)
forks = 50
# Smart fact gathering (cache results)
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible-facts
fact_caching_timeout = 86400
# Disable retry files
retry_files_enabled = False
[ssh_connection]
# SSH pipelining — biggest single improvement (~2x speed)
pipelining = True
# Persistent SSH connections
ssh_args = -C -o ControlMaster=auto -o ControlPersist=600s
# Transfer method
transfer_method = smart
Impact Benchmarks
| Optimization | Typical Speedup |
|---|---|
pipelining = True | 40-60% faster |
forks = 50 (from 5) | 5-10x faster on many hosts |
gathering = smart | 20-30% faster (cached facts) |
ControlPersist=600s | 10-20% faster (reuse SSH) |
Disable gather_facts | 2-5s saved per play |
SSH Pipelining
The single biggest improvement. Reduces SSH operations from 5+ per task to 1:
[ssh_connection]
pipelining = True
Requirement: requiretty must be disabled in /etc/sudoers:
- name: Disable requiretty for pipelining
ansible.builtin.lineinfile:
path: /etc/sudoers
regexp: "^Defaults.*requiretty"
line: "Defaults !requiretty"
validate: "visudo -cf %s"
Fact Caching
Don't gather facts every run:
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible-facts
fact_caching_timeout = 86400 # 24 hours
Or skip facts entirely when not needed:
- hosts: webservers
gather_facts: false # Saves 2-5 seconds
tasks: ...
Optimize Task Design
Combine Package Installs
# SLOW — 5 separate apt calls
- apt: { name: nginx }
- apt: { name: redis }
- apt: { name: git }
- apt: { name: curl }
- apt: { name: vim }
# FAST — 1 apt call
- ansible.builtin.apt:
name: [nginx, redis, git, curl, vim]
state: present
update_cache: true
cache_valid_time: 3600
Use async for Long Tasks
# Run long tasks in parallel
- name: Update packages (all hosts simultaneously)
ansible.builtin.apt:
upgrade: dist
async: 3600
poll: 0
register: update_job
# Do other work...
- name: Wait for updates
ansible.builtin.async_status:
jid: "{{ update_job.ansible_job_id }}"
until: result.finished
retries: 120
delay: 30
Use free Strategy
# Let each host proceed independently
- hosts: webservers
strategy: free # Don't wait for slowest host
tasks: ...
Profile Tasks
Find what's slow:
[defaults]
callbacks_enabled = timer, profile_tasks, profile_roles
Output:
===============================================================================
Install packages ------------------------------------------ 45.23s
Gather facts --------------------------------------------- 12.11s
Deploy configuration -------------------------------------- 3.45s
Reduce SSH Round-Trips
Use Modules Over command/shell
# SLOW — multiple SSH operations
- command: mkdir -p /opt/myapp
- command: chown deploy:deploy /opt/myapp
- command: chmod 755 /opt/myapp
# FAST — single module call
- ansible.builtin.file:
path: /opt/myapp
state: directory
owner: deploy
mode: "0755"
Batch template/copy Operations
# SLOW — multiple file transfers
- copy: { src: file1, dest: /etc/myapp/ }
- copy: { src: file2, dest: /etc/myapp/ }
- copy: { src: file3, dest: /etc/myapp/ }
# FASTER — synchronize entire directory
- ansible.posix.synchronize:
src: files/myapp/
dest: /etc/myapp/
Mitogen (Advanced)
Mitogen replaces Ansible's SSH-based execution with an optimized connection strategy — 2-7x faster:
pip install mitogen
[defaults]
strategy_plugins = /path/to/mitogen/ansible_mitogen/plugins/strategy
strategy = mitogen_linear
⚠️ Check compatibility with your Ansible version.
Scale to 1000+ Hosts
[defaults]
forks = 100
gathering = smart
fact_caching = redis
fact_caching_connection = localhost:6379:0
fact_caching_timeout = 86400
[ssh_connection]
pipelining = True
ssh_args = -C -o ControlMaster=auto -o ControlPersist=600s -o PreferredAuthentications=publickey
[inventory]
cache = True
cache_plugin = jsonfile
cache_timeout = 3600
Serial Batches for Large Fleets
- hosts: all
serial: "20%" # 20% of hosts at a time
tasks: ...
Pull Mode
For 1000+ hosts, consider ansible-pull — each host pulls and applies its own config:
# On each host (via cron)
ansible-pull -U https://github.com/org/ansible-repo.git site.yml
Optimization Checklist
| # | Optimization | Effort | Impact |
|---|---|---|---|
| 1 | Enable pipelining | 1 min | ⭐⭐⭐⭐⭐ |
| 2 | Increase forks | 1 min | ⭐⭐⭐⭐ |
| 3 | Enable fact caching | 2 min | ⭐⭐⭐ |
| 4 | SSH ControlPersist | 1 min | ⭐⭐⭐ |
| 5 | Combine package installs | 5 min | ⭐⭐⭐ |
| 6 | Skip gather_facts | 1 min | ⭐⭐ |
| 7 | Use free strategy | 1 min | ⭐⭐ |
| 8 | Profile tasks | 2 min | ⭐⭐ |
| 9 | async for long tasks | 10 min | ⭐⭐ |
| 10 | Mitogen | 15 min | ⭐⭐⭐⭐ |
Troubleshooting
Still Slow After Optimizations
# Profile to find the bottleneck
ANSIBLE_CALLBACKS_ENABLED=profile_tasks ansible-playbook site.yml
# Check SSH connection time
time ssh user@host "echo ok"
# Check DNS resolution (common hidden slowdown)
time nslookup myhost.example.com
Pipelining Not Working
# Verify pipelining is active
ansible-config dump | grep PIPELINING
# Should show: ANSIBLE_PIPELINING = True
Related Articles
Conclusion
The three biggest wins: SSH pipelining (40-60% faster), increase forks (5-10x on many hosts), and fact caching (20-30% faster). Apply these three changes and you'll see dramatic improvement immediately. For further gains, profile your tasks to find bottlenecks, combine operations, use async for long tasks, and consider Mitogen for 2-7x speedup. At scale (1000+ hosts), switch to pull mode and use Redis for fact caching.