Skip to main content
RunBook Academy

AnsibleXXXIV · Concurrency, Strategies and PerformanceConcurrency, strategies and performance

linear, free and host_pinned

Advanced⏱ ~25 minansible-core

What you'll learn

  • Name the four strategy plugins in ansible.builtin and what each one changes
  • Describe the task-level synchronisation barrier that linear provides
  • State what free removes and which operational guarantees depend on it
  • Decide when free is appropriate and when it is incompatible with the change being made

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

The default strategy has a property nobody notices until it is gone: when task 3 finishes, every host has finished task 3. That is a barrier, and a great deal of what this course teaches about staged, verifiable change rests on it.

strategy: free removes it. That is faster, and it is not a free improvement — it is a trade, and the thing being traded away is the reason a rolling deployment is meaningful rather than just parallel.

Four, and only four

Read-only / Safethe complete list in ansible.builtin
$ ansible-doc -t strategy -l
ansible.builtin.debug       Executes tasks in interactive debug session
ansible.builtin.free        Executes tasks without waiting for all hosts
ansible.builtin.host_pinned Executes tasks on each host without interruptio...
ansible.builtin.linear      Executes tasks in a linear fashion

Three of them are execution strategies and one is a debugger. strategy is a Play keyword only — it cannot be set per task or per block — and the default is linear, which ansible-config dump confirms as DEFAULT_STRATEGY(default) = linear.

linear: the barrier you are used to

One task at a time, across all hosts in the current batch, and the play does not advance until every host has finished that task or dropped out. forks decides how many of those hosts are worked on simultaneously; the barrier is unaffected by it.

Read-only / Safelinear: nobody moves on until everybody has
$ ansible-playbook -i strat.ini strat.yml -f 5
TASK [Task A (web1 is slow)] ***************************************************
ok: [web2]
ok: [web3]
ok: [web1]

TASK [Task B] ******************************************************************
ok: [web1] => { "msg": "B on web1" }
ok: [web2] => { "msg": "B on web2" }
ok: [web3] => { "msg": "B on web3" }

TASK [Task C] ******************************************************************
ok: [web1] => { "msg": "C on web1" }
ok: [web2] => { "msg": "C on web2" }
ok: [web3] => { "msg": "C on web3" }

web2 and web3 finished task A immediately and then waited six seconds for web1 before anyone started task B. That waiting is the cost, and it is exactly what free recovers.

Note also that the ok: lines under task A are in completion order — web2, web3, then web1. linear dispatches in parallel up to forks and prints results as they arrive; it is the advance to the next task that is synchronised, not the dispatch.

free: no barrier

Read-only / Safefree: each host runs ahead on its own
$ ansible-playbook -i strat.ini strat.yml -f 5 -e strat=free
TASK [Task A (web1 is slow)] ***************************************************
ok: [web2]
ok: [web3]

TASK [Task B] ******************************************************************
ok: [web2] => { "msg": "B on web2" }
ok: [web3] => { "msg": "B on web3" }

TASK [Task C] ******************************************************************
ok: [web2] => { "msg": "C on web2" }
ok: [web3] => { "msg": "C on web3" }

TASK [Task A (web1 is slow)] ***************************************************
ok: [web1]

TASK [Task B] ******************************************************************
ok: [web1] => { "msg": "B on web1" }

TASK [Task C] ******************************************************************
ok: [web1] => { "msg": "C on web1" }

web2 and web3 completed the entire play — A, B and C — before web1 finished task A. The task banner is reprinted when a host reaches a task the output has moved past.

The upstream description matches what was observed: “Ansible will not wait for other hosts to finish the current task before queuing more tasks for other hosts… a host that is slow or stuck on a specific task will not hold up the rest of the hosts and tasks.”

That output block is also the honest picture of what a free run looks like to read. Task banners repeat, hosts appear out of order, and there is no moment at which the transcript tells you the fleet is in a consistent state.

host_pinned

The description is precise about what it does and it is worth reading carefully, because the name suggests something narrower:

“Task execution is as fast as possible per host in batch as defined by serial (default all). Ansible will not start a play for a host unless the play can be finished without interruption by tasks for another host, i.e. the number of hosts with an active play does not exceed the number of forks. Ansible will not wait for other hosts to finish the current task before queuing the next task for a host that has finished. Once a host is done with the play, it opens its slot to a new host that was waiting to start. Other than that, it behaves just like the free strategy.”

So: free, with an additional rule that a host is only started when a worker slot is available for it to run to completion, and slots are recycled as hosts finish.

Where that earns its place is a play that holds a scarce resource for its whole duration — a licence checkout, a maintenance-window slot, a database connection from a small pool. Under free, forks hosts start and all of them hold the resource concurrently for as long as any of them is slow. Under host_pinned, the number of hosts with a play in progress stays bounded and each one finishes before another begins.

It is still barrier-free. Everything the callout above says about staged changes applies to host_pinned unchanged.

debug

Not a performance strategy. It drops into an interactive debugger when a task fails, letting you inspect and re-run it. It forces serialised execution as a side effect of being interactive, which makes it useless for anything except debugging — and the debugging technique itself belongs to this course’s troubleshooting part.

Do not leave strategy: debug in a committed playbook. A run that stops for an interactive prompt in an unattended context is a run that hangs until someone notices.

Knowledge check

Knowledge check · 4 questions

  1. Q1. Three hosts run a play of tasks A, B and C. web1 takes six seconds on A; web2 and web3 take none. Under strategy: free, what does the run look like?

  2. Q2. Why is strategy: free inappropriate for a play that drains a host, deploys, health-checks and returns it to the pool?

  3. Q3. Which statements about the strategy plugins in ansible.builtin are correct? Select all that apply.

  4. Q4. Under linear, the completion of a task means every host in the current batch has finished that task or has dropped out of the play.

Passing score: 75%. Answers are checked in this browser.