Batching and Throttling
A change that is correct on one host and catastrophic on sixty is the failure mode this topic prevents. serial bounds how many hosts a bad release reaches per wave, max_fail_percentage aborts the whole play once too many hosts in a batch fail, throttle caps a single dangerous task, and --limit carves the run down to a subset. Together they are the guardrails that stop a push-based tool from pushing a mistake to the entire fleet at once.
These are complementary, not alternatives — each addresses a different way a fleet-wide change goes wrong, and a serious production rollout layers all four. The topic takes them one at a time, then shows how they stack.
serial as Blast-Radius Control
Beyond rolling deploys, serial is the primary limit on how many hosts a change can break before you notice. It takes a count, a list, or a percentage, and whatever the form, the effect is the same: a bad config at serial: 1 fails on one host, not all six. The wave size is the blast radius. Choosing it is choosing how much damage a wrong change is allowed to do before the run reaches a point where you can stop it.
max_fail_percentage — Stop the Bleed
max_fail_percentage: 20, paired with serial, aborts the entire play once more than twenty percent of a batch fails. A release that breaks the first two hosts of a wave never proceeds to the remaining four — the play stops cold. This matters because the default behavior is the opposite: without it, Ansible keeps applying a broken change to every host in the batch until they have all failed, one after another, before it gives up. max_fail_percentage turns "fail everything, then report" into "fail a little, then halt."
- hosts: web serial: 2 # two hosts per wave max_fail_percentage: 20 # abort the whole play if >20% of a wave fails become: true tasks: - name: Deploy the new release ansible.builtin.git: repo: https://github.com/larkspur-io/app.git dest: /opt/larkspur version: "{{ release_tag }}"
throttle — Cap One Hot Task
throttle: 2 on a specific task limits it to two hosts at a time regardless of forks or serial. It is for the one step that hammers a shared resource — a license server, a package mirror, an upstream API — that would buckle if the whole batch hit it simultaneously. throttle is scoped to the task, not the play: the rest of the wave runs at full width, and only the dangerous task is squeezed down to a rate the shared resource can survive.
--limit Batches — Manual Carving
--limit web1.larkspur.io:web2.larkspur.io runs the play only against the named subset, and --limit @retry_file runs it only against the hosts a previous run recorded as failed. This is the operator-driven complement to serial — a human carving the run by hand to canary a change on two named hosts, or to re-run just the hosts that failed instead of the whole fleet. Where serial batches automatically, --limit is you drawing the boundary for one invocation.
Layering the Guards
The four guards stack because each catches a different failure. serial sets the wave size so a bad change reaches only a few hosts at a time. max_fail_percentage sets the abort threshold so a wave that starts failing halts the rollout. throttle protects the one task that pulls on a shared, rate-limited resource. --limit is the human override for canarying or re-running failures. Use one where you meant another and you either flood a shared resource, slow the whole play needlessly, or let a broken change roll on past the point it should have stopped.
There is a sharp ordering dependency between the first two. max_fail_percentage is evaluated per batch, so it only does its job when serial has created batches to evaluate against. Set the threshold without batching and it is measured against the whole play at once, which defeats the early-abort intent on a large run. The two are a pair: serial creates the waves, max_fail_percentage decides when a wave's failures stop the rollout.
- Running a fleet-wide play with
serialbut nomax_fail_percentage— the batch keeps applying a broken change to every host in the wave even after the first hosts fail, because the default is to continue until all fail. - Setting
max_fail_percentagewithoutserial— with no batching the threshold is evaluated against the whole play at once, which defeats the early-abort intent on a large run. - Confusing
throttlewithserial—throttlelimits one task's concurrency within a wave,seriallimits the wave size; using one where you meant the other either floods a shared resource or needlessly slows the whole play. - Hammering a shared package mirror by deploying sixty hosts at full
forkswith nothrottleon the fetch task — the mirror rate-limits or falls over and the deploy fails mid-fleet. - Trusting
--limitto persist — it applies to one invocation, so the next run without it is back to the full fleet; an exclusion is not a permanent fence.
- Pair
serialwithmax_fail_percentageon every production rollout, so a wave that starts failing aborts before it touches the rest of the fleet. - Set a low
max_fail_percentage— single digits — on critical tiers; one or two failed hosts in a batch should stop the rollout, not be tolerated. - Add
throttle: Nto any task that pulls on a shared, rate-limited resource, sizing N to what that resource can take rather than to the batch size. - Use
--limitwith the generated retry file to re-run only the hosts that failed, instead of re-applying the play to the whole fleet after a partial failure. - Confirm scope with
--list-hostsbefore a--limitrun against prod, so the subset you carved is the subset that actually resolves.
maxUnavailable plus a failure threshold, the closest analog to serial + max_fail_percentage
Spinnaker canary and automated rollback at the pipeline layer
Terraform no batch-and-abort primitive for applying a change across many resources
Knowledge Check
Why does serial without max_fail_percentage still flood a wave with a broken change?
- With no abort threshold set, the play keeps applying the change to every host in the wave until they have all failed
serialonly batches the connections, not the play itself, so every host in the fleet still receives the change at onceserialis silently ignored at runtime unlessmax_fail_percentageis also set alongside it on the play- The first host failure silently widens the current wave to take in the rest of the whole fleet
What does max_fail_percentage evaluate against, and when does it work as intended?
- It is evaluated per batch, so it delivers early abort only when
serialhas created batches to measure against - It is evaluated against the control node's
forkscount, completely independent of whateverserialis set to - It is evaluated just once at the very end of the play, after every host in the fleet has already run
- It is evaluated against the whole fleet at once and works best with no
serialbatching at all
What is the difference between throttle and serial?
throttlecaps one task's concurrency within a wave;serialcaps the wave size for the whole playthrottlesets the size of each wave for the whole play whileserialcaps concurrency on a single task- They are really the same underlying setting, just applied at two different scopes in the play
throttleaborts the whole play the moment a task fails whileserialonly slows the rollout down
Does a --limit carve persist across runs?
- No — it applies to one invocation, so the next run without it is back to the full fleet
- Yes — it writes a permanent host exclusion directly into the inventory file on disk
- Yes — it persists across every later run on this control node until the node is next rebooted
- It persists across runs only when it is combined with a
serialbatch size
You got correct