Concurrency and Order
This closing chapter covers the scale, pace, and sequence of multi-host execution — four genuinely different controls, often confused with each other, each solving a different problem.
forks: How Many Hosts Run In Parallel
By default, Ansible runs tasks against up to 5 hosts simultaneously. With more hosts than that in your target group, they’re processed in batches of that size. forks controls the batch size:
ansible-playbook site.yaml --forks 20Or persistently, recalling the configuration chapter:
[defaults]
forks = 20Higher forks means more parallelism and a faster overall run against many hosts — but also more simultaneous load on the control node itself, and potentially on any shared resource the managed hosts touch, like a database receiving 20 simultaneous connection attempts instead of 5.
serial: Rolling Batches Instead Of All At Once
forks is about raw connection concurrency. serial is a genuinely different, play-level control: how many hosts complete the entire play before the next batch even begins.
- name: Rolling Deployment
hosts: servers
serial: 2
tasks:
- name: Deploy new version
ansible.builtin.debug:
msg: "Deploying to {{ inventory_hostname }}"
- name: Verify health
ansible.builtin.debug:
msg: "Verifying {{ inventory_hostname }}"With serial: 2 against 6 total hosts, Ansible runs 2 hosts through the entire task list, then the next 2, then the final 2 — never all 6 at once. This isn’t about connection speed; it’s about limiting the blast radius of a deployment.
A Realistic Example: A Rolling Deployment
Deploying a new — possibly broken — version to every web server simultaneously means a total outage the moment something’s wrong. With serial: 2 out of 6 servers, only 2 are ever mid-update at any given moment, while the other 4 keep serving traffic normally.
serial also accepts a percentage, or a list of increasing batch sizes:
serial: "25%"serial:
- 1
- 3
- "50%"The list form is a common “start small, build confidence” pattern: one host first (the smallest possible risk), then three, then half the remainder at a time.
throttle: Limiting Parallelism For One Specific Task
forks sets overall play-wide parallelism. throttle limits parallelism for just one task, without touching every other task in the play:
- name: Call a rate-limited API
ansible.builtin.debug:
msg: "Calling API for {{ inventory_hostname }}"
throttle: 1Even with forks: 20 allowing broad concurrency generally, this specific task only ever runs on one host at a time — useful for a task hitting a rate-limited external API or another shared resource, without slowing the entire play down to match.
order:: Controlling Which Sequence Hosts Are Visited In
By default, hosts are processed in the order they appear in the inventory. order: changes this:
- name: My Play
hosts: servers
order: sorted
tasks:
- name: Example task
ansible.builtin.debug:
msg: "..."The order: Options: inventory, reverse_inventory, sorted, shuffle
inventory(default) — the order hosts appear in the inventory source.reverse_inventory— exactly the opposite.sorted— alphabetical by hostname.reverse_sorted— reverse alphabetical.shuffle— genuinely random each run, useful for avoiding a systematic bias — if whichever host happens to be first in inventory order is subtly different in some way, always hitting it first every single run can mask or reveal issues inconsistently.
Note
inventory does not mean the order in which hosts/groups are defined in inventory file — it’s the order of selection retured from the compiled inventory by Ansible which may be reproducible but not predictible.
A Realistic Pattern: Canary Deployment With serial And run_once
Combining this chapter’s serial with the previous chapter’s run_once, plus until from the loops section and template/systemd from common modules:
- name: Canary Deployment
hosts: servers
serial:
- 1
- "100%"
tasks:
- name: Deploy new version
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: Restart app
- name: Wait for health check to pass
ansible.builtin.command: curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health
register: health_check
until: health_check.stdout == "200"
retries: 10
delay: 3
handlers:
- name: Restart app
ansible.builtin.systemd:
name: myapp
state: restartedThe first batch — a single canary host — deploys and verifies. If that batch fails, the whole play stops right there by default, before the remaining batch is ever touched — protecting every other host from a genuinely broken deployment. Only once the canary succeeds does execution proceed to the second batch, "100%" of whatever remains.
Best Practices
- Use
forksto match overall parallelism to control-node and target capacity — higher for many lightweight hosts, lower for heavier per-host operations. - Use
serialwhenever limiting blast radius genuinely matters — start with a small canary batch and increase gradually rather than deploying to everything at once. - Use
throttlefor one specific task hitting a shared or rate-limited resource, without throttling the entire play to match. - Use
order: shuffleto avoid systematic bias from always processing hosts in the same fixed sequence, andsorted/reverse_sortedwhen a predictable, human-readable sequence matters more. - Combine a small first
serialbatch with a health-check task for genuine canary-deployment safety, rather than trusting a deployment succeeded just because no task reported an error.