Introduction
Ansible's execution strategy controls how tasks run across multiple hosts. The default linear strategy runs each task on all hosts before moving to the next. free lets each host proceed independently. serial limits how many hosts are updated at once — essential for zero-downtime deployments.
Strategy Types
Linear (Default)
Every host completes task 1 before any host starts task 2:
- hosts: webservers
strategy: linear # Default — can omit
tasks:
- name: Task 1 # Runs on ALL hosts first
...
- name: Task 2 # Then ALL hosts run this
...
Host A: [Task 1] ─── wait ─── [Task 2] ─── wait ─── [Task 3]
Host B: [Task 1] ─── wait ─── [Task 2] ─── wait ─── [Task 3]
Host C: [Task 1] ─── wait ─── [Task 2] ─── wait ─── [Task 3]
▲ sync point ▲ sync point
Free Strategy
Each host proceeds at its own pace:
- hosts: webservers
strategy: free
tasks:
- name: Task 1
...
- name: Task 2
...
Host A: [Task 1] [Task 2] [Task 3] ──── done
Host B: [Task 1] ──── [Task 2] ──── [Task 3] ── done
Host C: [Task 1] [Task 2] ──── [Task 3] ── done
▲ no sync points, each host runs independently
Use free when:
- Tasks are independent per host
- Hosts have different speeds
- You want maximum parallelism
Serial (Rolling Updates)
Process hosts in batches:
# Update 2 hosts at a time
- hosts: webservers
serial: 2
tasks:
- name: Deploy
...
Batch 1: [Host A, Host B] → run all tasks
Batch 2: [Host C, Host D] → run all tasks
Batch 3: [Host E] → run all tasks
Serial with Percentages
# Update 25% of hosts at a time
- hosts: webservers
serial: "25%"
tasks: ...
Progressive Serial (Canary)
# Start small, scale up
- hosts: webservers
serial:
- 1 # First: 1 host (canary)
- 5 # Then: 5 hosts
- "25%" # Then: 25% at a time
tasks: ...
This is perfect for canary deployments — test on 1 host, then 5, then roll out to the rest.
max_fail_percentage
Stop the deployment if too many hosts fail:
- hosts: webservers
serial: 5
max_fail_percentage: 20 # Stop if >20% fail in a batch
tasks:
- name: Deploy application
...
# Absolute number
- hosts: webservers
serial: 10
any_errors_fatal: true # Stop on ANY failure
tasks: ...
Practical Patterns
Zero-Downtime Rolling Deploy
---
- hosts: webservers
serial: 1 # One host at a time
max_fail_percentage: 0 # Zero tolerance
pre_tasks:
- name: Remove from load balancer
ansible.builtin.uri:
url: "https://lb.example.com/api/disable/{{ inventory_hostname }}"
method: POST
headers:
Authorization: "Bearer {{ lb_token }}"
delegate_to: localhost
tasks:
- name: Stop application
ansible.builtin.service:
name: myapp
state: stopped
- name: Deploy new version
ansible.builtin.copy:
src: "myapp-{{ version }}.jar"
dest: /opt/myapp/
- name: Start application
ansible.builtin.service:
name: myapp
state: started
- name: Wait for health check
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
retries: 30
delay: 5
post_tasks:
- name: Add back to load balancer
ansible.builtin.uri:
url: "https://lb.example.com/api/enable/{{ inventory_hostname }}"
method: POST
headers:
Authorization: "Bearer {{ lb_token }}"
delegate_to: localhost
Canary Deployment
---
- hosts: webservers
serial:
- 1 # Canary: 1 host
- "100%" # If canary passes, all remaining
max_fail_percentage: 0
tasks:
- import_tasks: tasks/deploy.yml
- name: Run smoke tests
ansible.builtin.command: /opt/myapp/smoke-test.sh
delegate_to: localhost
Database Migration (Single Run)
# Run migration on ONE host only
- hosts: dbservers
serial: 1
tasks:
- name: Run migration
ansible.builtin.command: /opt/app/migrate.sh
run_once: true
- name: Restart all app servers
ansible.builtin.service:
name: myapp
state: restarted
Maintenance Window
# Fast parallel for maintenance tasks
- hosts: all
strategy: free
gather_facts: false
tasks:
- name: Apply security patches
ansible.builtin.apt:
upgrade: safe
update_cache: true
- name: Reboot if needed
ansible.builtin.reboot:
when: reboot_required_file.stat.exists
Comparison
| Feature | linear | free | serial |
|---|---|---|---|
| Sync between tasks | Yes | No | Yes (per batch) |
| Host independence | No | Yes | Per batch |
| Rolling update | No | No | Yes |
| Canary deploy | No | No | Yes |
| Speed | Medium | Fastest | Slowest (safest) |
| Best for | General use | Independent tasks | Production deploys |
Throttle (Per-Task Limit)
# Limit concurrency for a specific task
- name: Hit API (rate limited)
ansible.builtin.uri:
url: https://api.example.com/deploy
throttle: 3 # Max 3 concurrent
Troubleshooting
Serial Too Slow
Increase batch size:
# Instead of serial: 1
serial: "20%" # 20% of hosts at a time
Free Strategy Causes Race Conditions
If tasks depend on other hosts' state, use linear:
# Free is dangerous when tasks depend on each other
strategy: linear # Use when tasks have cross-host dependencies
Related Articles
Conclusion
Use linear (default) for most playbooks. Use free when tasks are independent and you want speed. Use serial for production deployments — it's the key to zero-downtime rolling updates. Combine serial with progressive batches (1, 5, 25%) for canary deployments. Always set max_fail_percentage in production to stop bad deployments early. Use throttle to limit concurrency on specific tasks (API rate limits).