Introduction

Slow Ansible playbooks waste time and delay deployments. A playbook that takes 30 minutes could run in 5 minutes with the right optimizations. Whether you manage 10 servers or 10,000, performance tuning is essential.

This guide covers 10 proven methods to speed up Ansible playbooks, from quick ansible.cfg tweaks to architectural changes that dramatically reduce execution time.

1. Measure Before Optimizing (Callback Plugins)

Before optimizing, identify what's actually slow. Enable timing callback plugins in ansible.cfg:

[defaults]
callbacks_enabled = timer, profile_tasks, profile_roles

# Optional: set display format
[callback_profile_tasks]
task_output_limit = 20
sort_order = descending

Output example:

PLAY RECAP ****
Wednesday 20 April 2026  22:15:00 +0000 (0:00:02.156)  0:04:23.891 ****
===============================================================================
Install packages ----------------------------------------- 180.23s
Copy configuration files ---------------------------------- 45.12s
Restart services ------------------------------------------ 12.45s
Gather facts ---------------------------------------------- 8.76s

Now you know exactly where to focus your optimization efforts.

2. Disable Fact Gathering

Fact gathering (setup module) runs on every play by default and takes 2-10 seconds per host. If you don't use ansible_facts, disable it:

---
- name: Deploy application
  hosts: webservers
  gather_facts: false  # Save 2-10 seconds per host
  tasks:
    - name: Copy new artifact
      ansible.builtin.copy:
        src: app.jar
        dest: /opt/app/app.jar

Selective Fact Gathering

If you only need specific facts, gather a subset:

---
- name: Only network facts
  hosts: all
  gather_facts: true
  gather_subset:
    - network
    - "!hardware"
    - "!virtual"
  tasks:
    - name: Show IP
      ansible.builtin.debug:
        var: ansible_default_ipv4.address

Fact Caching

If you need facts across multiple plays, cache them instead of re-gathering:

[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 3600  # Cache for 1 hour

With gathering = smart, Ansible only gathers facts if they're not in the cache.

3. Increase Parallelism (Forks)

The forks setting controls how many hosts Ansible manages simultaneously. Default is 5 — far too low for most environments:

[defaults]
forks = 50  # Manage 50 hosts in parallel

Guidelines:

EnvironmentRecommended Forks
Laptop/Dev10-20
CI/CD server30-50
Dedicated Ansible controller50-100
High-memory controller100-500

Each fork uses ~100-200 MB of memory. Monitor your controller's RAM usage.

4. Enable SSH Pipelining

By default, Ansible opens multiple SSH connections per task (copy module → execute → fetch result). Pipelining reduces this to one connection:

[connection]
pipelining = true

Requirement: The target's /etc/sudoers must have requiretty disabled (it is on most modern systems). If you see errors, add:

Defaults !requiretty

Impact: 2-5x speed improvement for playbooks with many tasks.

5. Optimize SSH Connections (Multiplexing)

SSH multiplexing reuses a single TCP connection for multiple SSH sessions:

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o PreferNetworkAddress=yes
control_path_dir = ~/.ansible/cp
control_path = %(directory)s/%%h-%%r

What this does:

  • ControlMaster=auto — Reuse existing connections
  • ControlPersist=60s — Keep connection alive for 60 seconds after last session
  • Eliminates TCP handshake + SSH authentication for subsequent tasks

6. Use Strategy: free

The default linear strategy waits for ALL hosts to complete each task before moving to the next. The free strategy lets fast hosts race ahead:

---
- name: Deploy with free strategy
  hosts: webservers
  strategy: free
  tasks:
    - name: Update packages
      ansible.builtin.yum:
        name: "*"
        state: latest

    - name: Restart service
      ansible.builtin.systemd:
        name: httpd
        state: restarted

When to use: Tasks that are independent per host (package installs, restarts). When NOT to use: Tasks that depend on other hosts' state (cluster operations, rolling updates).

7. Use Async for Long-Running Tasks

Long tasks (package updates, database migrations) block all other operations. Use async to run them in the background:

---
- name: Parallel long operations
  hosts: all
  tasks:
    - name: Update all packages (async)
      ansible.builtin.yum:
        name: "*"
        state: latest
      async: 600    # Allow up to 10 minutes
      poll: 0       # Fire and forget
      register: yum_job

    - name: Do other quick tasks while packages update
      ansible.builtin.copy:
        src: motd.txt
        dest: /etc/motd

    - name: Wait for package update to finish
      ansible.builtin.async_status:
        jid: "{{ yum_job.ansible_job_id }}"
      register: job_result
      until: job_result.finished
      retries: 60
      delay: 10

8. Use Serial for Rolling Updates (Not for Speed, but Control)

While serial limits parallelism (seemingly slower), it prevents downtime during rolling deployments:

---
- name: Rolling deployment
  hosts: webservers
  serial: "25%"  # Deploy to 25% of hosts at a time
  tasks:
    - name: Deploy new version
      ansible.builtin.copy:
        src: app-v2.jar
        dest: /opt/app/app.jar
      notify: Restart app

  handlers:
    - name: Restart app
      ansible.builtin.systemd:
        name: myapp
        state: restarted

Combine with max_fail_percentage to abort if too many hosts fail:

  serial: 5
  max_fail_percentage: 20

9. Optimize Task Design

Avoid Loops Where Modules Accept Lists

# SLOW — runs yum 5 times (5 SSH round-trips)
- name: Install packages (slow)
  ansible.builtin.yum:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - redis
    - postgresql
    - python3
    - git

# FAST — single yum transaction
- name: Install packages (fast)
  ansible.builtin.yum:
    name:
      - nginx
      - redis
      - postgresql
      - python3
      - git
    state: present

Use changed_when: false for Check Commands

# Prevents unnecessary handler triggers
- name: Check service status
  ansible.builtin.command: systemctl is-active nginx
  register: nginx_status
  changed_when: false
  failed_when: false

Avoid shell When a Module Exists

# SLOW — spawns shell, no idempotency
- name: Create user
  ansible.builtin.shell: useradd -m deploy

# FAST — native module, idempotent, no shell overhead
- name: Create user
  ansible.builtin.user:
    name: deploy
    create_home: true

10. Mitogen (Advanced)

Mitogen replaces Ansible's default SSH connection with a high-performance Python-to-Python channel. It can provide 2-7x speedup:

[defaults]
strategy_plugins = /path/to/mitogen/ansible_mitogen/plugins/strategy
strategy = mitogen_linear

Benefits:

  • Eliminates temporary file transfers
  • Reduces SSH round-trips
  • Compresses data in transit
  • Connection persistence built-in

Caveat: Not officially supported by Red Hat; test thoroughly before production use.

Quick Reference: ansible.cfg for Performance

[defaults]
forks = 50
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 3600
callbacks_enabled = timer, profile_tasks
host_key_checking = false
internal_poll_interval = 0.001

[connection]
pipelining = true

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
control_path_dir = ~/.ansible/cp
control_path = %(directory)s/%%h-%%r

Performance Impact Summary

OptimizationExpected SpeedupEffort
Disable fact gathering5-15%Low
Increase forks10-50%+Low
Enable pipelining20-50%Low
SSH multiplexing10-30%Low
Strategy: free20-40%Low
Fact caching5-15%Medium
Async tasks30-60%Medium
Module lists vs loops10-30%Medium
Mitogen100-600%High

Conclusion

The fastest wins come from combining forks, pipelining, and SSH multiplexing — three ansible.cfg changes that typically cut execution time in half. Add gather_facts: false where possible and pass lists to modules instead of using loops. For maximum performance, evaluate Mitogen for your environment. Always measure with callback plugins before and after changes to verify improvements.