Chapter 10: Orchestration & Scale
Topic 61

Batching and Throttling

ScaleSafety

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."

serial plus max_fail_percentage — batch the rollout, abort on early failure
- 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.

Common Mistakes
  • Running a fleet-wide play with serial but no max_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_percentage without serial — with no batching the threshold is evaluated against the whole play at once, which defeats the early-abort intent on a large run.
  • Confusing throttle with serialthrottle limits one task's concurrency within a wave, serial limits 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 forks with no throttle on the fetch task — the mirror rate-limits or falls over and the deploy fails mid-fleet.
  • Trusting --limit to 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.
Best Practices
  • Pair serial with max_fail_percentage on 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: N to 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 --limit with 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-hosts before a --limit run against prod, so the subset you carved is the subset that actually resolves.
Comparable tools Kubernetes rolling-update 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
  • serial only batches the connections, not the play itself, so every host in the fleet still receives the change at once
  • serial is silently ignored at runtime unless max_fail_percentage is 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 serial has created batches to measure against
  • It is evaluated against the control node's forks count, completely independent of whatever serial is 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 serial batching at all

What is the difference between throttle and serial?

  • throttle caps one task's concurrency within a wave; serial caps the wave size for the whole play
  • throttle sets the size of each wave for the whole play while serial caps concurrency on a single task
  • They are really the same underlying setting, just applied at two different scopes in the play
  • throttle aborts the whole play the moment a task fails while serial only 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 serial batch size

You got correct