Ansible Guide
Flat isometric illustration of three pink server towers on a dark speckled platform, the tallest fanning white lines down to small pink pads and dots.
Execution

Ansible Strategies: linear vs free vs host_pinned

Compare Ansible's linear, free, and host_pinned strategies: execution order, tradeoffs, forks, serial, and when each strategy fits.

By Ansible Guide Editorial · · 7 min read

The shape of an Ansible run is set by one play keyword most people never touch. The documentation states the default plainly: Ansible runs each task on all hosts affected by a play before starting the next task on any host, using five forks. That single sentence explains why a play against 400 mixed hosts can take far longer than the sum of its work, and the strategy keyword is what changes it.

A strategy sets ordering, not capacity

The most common misreading is treating strategy as a speed dial. It is not. A strategy plugin decides the order in which task and host pairs are handed to workers. The fork count decides how many of those pairs can be in flight at once, and its documented default is 5.

Setting strategy: free on a play that still runs with five forks does not touch more hosts at a time. It changes which host gets the next slot when one frees up. Concurrency is a separate control, covered in how Ansible runs a playbook, and the two settings are easy to confuse because both are edited in the same [defaults] stanza.

linear: lockstep, and the reason it is the default

Under linear, every host in the batch completes task one before any host starts task two. Up to the fork limit of hosts execute a task simultaneously, then the next set, until the batch is done and the play advances.

The barrier at each task boundary is not an accident. Several things depend on it:

  • Cross-host templating. A task that reads hostvars for peer machines, such as rendering a cluster member list or a load balancer pool, needs those peers to have already reached the task that set the value. Lockstep guarantees that; nothing else does.
  • Readable output. Results arrive grouped under a task header, so a failure is attributable at a glance.
  • Batch-level error handling. Keywords such as any_errors_fatal and max_fail_percentage are evaluated against a batch that has finished the same task. Without a shared boundary the question “how many hosts failed this task” has no clean answer yet.

The cost is idle capacity. If 39 hosts finish a package install in two seconds and the fortieth takes ninety, the whole play waits ninety seconds at that task. Repeat that across 25 tasks and the slowest host has paced the entire run.

free: every host races to the end of the play

The free strategy plugin documents its behaviour as task execution being as fast as possible per batch, where the batch is defined by serial and defaults to all hosts. Ansible does not wait for other hosts to finish the current task before queuing more tasks for hosts that are already done.

Practically, each host walks the play at its own pace. A fast host may be on task 20 while a slow one is still on task 3. On an inventory with a wide spread of hardware, network latency or existing drift, that recovers most of the time linear spends waiting.

What you give up is real:

  • Interleaved output. Task headers no longer group results, and a long play against a large inventory produces output that is hard to follow live. Plan on reading a log rather than a terminal.
  • No shared task boundary. Any assumption that all hosts have passed a given point is invalid. Peer hostvars lookups are the classic breakage, and they fail quietly by rendering an empty or partial list rather than erroring.
  • Late failure visibility. A host that hangs on task 4 is less obvious when other hosts are streaming task 20 results past it.

free suits plays where hosts are genuinely independent: patching, log shipping, agent installs, anything with no cross-host coordination.

host_pinned: bounded parallelism with clean completion

host_pinned is the least known of the three and often the best fit for change work. Its plugin documentation describes execution as fast as possible per host in the batch, with an important constraint: Ansible will not start a play for a host unless the play can be finished without interruption by tasks for another host, meaning the number of hosts with an active play never exceeds the number of forks. It does not wait for other hosts before queuing the next task for a host that has finished, and once a host completes the play it opens its slot to a host that was waiting to start. Other than that, it behaves like free.

The difference matters when a run is interrupted. Under free, all hosts are somewhere in the middle of the play, so an abort leaves the entire inventory partially configured. Under host_pinned, at most forks hosts are mid-play; the rest are either finished or untouched. For a change with a half-applied state that is awkward to reason about, that is a materially safer failure mode for the same wall-clock cost.

debug: interactive, and not for automation

Ansible also ships a debug strategy, which drops into the task debugger when a task fails so variables can be inspected and the task retried in place. It is a troubleshooting tool for a terminal. A scheduled or pipeline run that sets it will block on a prompt nobody is watching.

Where a strategy is set, and what overrides what

A strategy can be set three ways: as a play keyword, in ansible.cfg under [defaults] as strategy = free, or through the ANSIBLE_STRATEGY environment variable. The documented default is linear.

The precedence rules matter here because teams frequently set a global strategy and then debug an individual play that ignores it. Ansible’s precedence appendix orders its categories, lowest to highest, as configuration settings, then command-line options, then playbook keywords, then variables. Each category overrides every lower one, so a play that declares strategy: linear beats an ansible.cfg that sets strategy = free. That is the correct escape hatch for a coordination-sensitive play inside a fleet-wide free configuration.

serial, throttle, order and run_once are not strategies

The strategies page is explicit that these keywords are directives applied to a play, block or task rather than strategies, and the distinction avoids a common design error.

  • serial sets a batch size, as a number, a percentage, or a list of numbers. Ansible completes the whole play on that batch before starting the next. This is the rolling-update control, and it composes with any strategy, because both free and host_pinned define their behaviour as fastest-possible within the batch that serial defines.
  • throttle caps the workers allotted to a block or task, which is how you keep one API-calling task inside a rate limit while the rest of the play runs wide.
  • order controls how the next host in a group is selected.
  • run_once runs a task on a single host.

So serial: 10 with strategy: host_pinned is not contradictory. It means ten hosts per batch, each of them running the play through to completion without interruption.

Choosing one

  • Hosts must coordinate, or output legibility matters, or the play is short: stay on linear.
  • Hosts are independent and per-host duration varies widely: free.
  • Hosts are independent but a partially applied change is expensive: host_pinned.
  • The change is risky regardless of strategy: add serial, which is a blast-radius control rather than a speed control.

Strategy will not fix a capacity problem

If a run is slow because five forks are being asked to cover 400 hosts, no strategy change will help; the width is wrong. Raise forks in steps against a control node you are watching, enable SSH pipelining to cut per-task round trips, and stop gathering facts on plays that do not read them. The inventory sizing calculator on this site models the fork, controller memory and batch-runtime relationship, and the ansible-playbook flag reference covers -f and the diagnostic options for finding where the time actually goes.

Common mistakes

Setting strategy = free globally and then losing peer hostvars values in a template, with no error to explain it. Treating serial and strategy as alternatives instead of composable controls. Raising forks alongside free until the control node saturates, which slows every host at once. Reading interleaved free output as evidence of a fault. Leaving the debug strategy in a play that later runs unattended. Choosing free for a change where a half-applied state is expensive, when host_pinned gives the same throughput with a much better abort story.

Which hosts a strategy applies to is a separate question, and it is answered by inventory groups, variables and patterns.

Sources

  1. Ansible documentation: Controlling playbook execution: strategies and more
  2. Ansible documentation: ansible.builtin.free strategy plugin
  3. Ansible documentation: ansible.builtin.host_pinned strategy plugin
  4. Ansible documentation: Controlling how Ansible behaves: precedence rules
#ansible #playbooks #strategies#performance #devops

Related