AnsibleXXXIV · Concurrency, Strategies and PerformanceConcurrency, strategies and performance
throttle: protecting shared dependencies
What you'll learn
- Use throttle to bound concurrency for the tasks that touch a shared dependency
- State the relationship between throttle, forks and serial, and demonstrate it
- Distinguish throttle from serial and from run_once by what each one controls
- Apply throttle at block level so a whole critical section is serialised
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
The previous lesson named three ceilings on a run: the controller, the rate
at which the fleet accepts connections, and the shared things every host
depends on. forks is the lever for the first. throttle is the lever for
the third, and it is the only one of the concurrency keywords that is about
a machine which is not in your play.
The shape it addresses:
Two hundred hosts run a package task. All two hundred fetch from one internal mirror. The mirror serves five concurrent clients comfortably, forty badly, and two hundred not at all — and the errors come back as timeouts that look like a network problem.
Lowering forks would fix it and would slow down every other task in the
run. throttle lowers concurrency for the tasks that need it and leaves
the rest alone.
What it does, measured
Four hosts, one task that takes two seconds each, run four ways:
$ /usr/bin/time -f 'elapsed %e s' ansible-playbook -i thr.ini thr.yml -f <N> [-e thr=<N>]-- forks 5, no throttle --
elapsed 2.48 s
-- forks 5, throttle 1 --
elapsed 8.91 s
-- forks 1, no throttle --
elapsed 8.92 s
-- forks 1, throttle 4 (cannot raise above forks) --
elapsed 8.91 sFour hosts times two seconds is eight seconds of work. Done in parallel it
takes 2.5 seconds; serialised it takes 8.9. throttle: 1 at forks: 5
produced the serialised time, which is the keyword working.
The fourth measurement is the one worth keeping. throttle: 4 with
forks: 1 still took 8.9 seconds — asking for more concurrency than
forks allows achieves nothing. The keyword documentation states it
directly:
$ ansible-doc -t keyword throttlethrottle:
applies_to:
- Play
- Role
- Block
- Task
- Handler
description: Limit the number of concurrent task runs on task, block and playbook
level. This is independent of the forks and serial settings, but cannot be set
higher than those limits. For example, if forks is set to 10 and the throttle
is set to 15, at most 10 hosts will be operated on in parallel.
priority: 0
template: explicit
type: intthrottle only ever lowers concurrency. It is a floor-ward
adjustment, never a ceiling-ward one, which makes it safe to add: the worst
case of a wrong value is a run that is slower than it needed to be.
Where to put it
The keyword applies to Play, Role, Block, Task and Handler, and the level you choose changes what is protected.
On the task
The narrow, common case. One task touches the shared thing; everything else in the play runs at full concurrency.
- name: Install the agent from the internal mirror
ansible.builtin.package:
name: monitoring-agent
state: present
throttle: 5On a block
The case people miss. If a sequence of tasks holds the shared resource, throttling one of them leaves the others racing.
- name: Licensed reconfiguration
block:
- name: Check out a licence seat
ansible.builtin.uri:
url: "https://licence.example.com/api/checkout"
method: POST
register: seat
- name: Apply the licensed configuration
ansible.builtin.command: /opt/vendor/bin/reconfigure --seat {{ seat.json.id }}
- name: Return the seat
ansible.builtin.uri:
url: "https://licence.example.com/api/release/{{ seat.json.id }}"
method: POST
throttle: 1With throttle: 1 on the individual checkout task instead, host A would
check out a seat, and while it was running reconfigure, host B would be
free to check out the next one. The pool would empty exactly as fast as it
would have without the keyword.
On a handler
Handlers inherit nothing from the tasks that notified them, so a handler
that restarts a service which registers with a shared discovery system
needs its own throttle:
handlers:
- name: restart app
ansible.builtin.systemd:
name: app
state: restarted
throttle: 4
On a play or role
Blunt but occasionally right — a role that exists entirely to talk to one
API, for example. Prefer the narrowest level that covers the resource,
because a play-level throttle slows down every task in the play including
the ones that touch nothing shared.
throttle, serial and run_once are three different questions
They get confused constantly, and the confusion produces real defects.
| Keyword | Question it answers | Effect on a 400-host play |
|---|---|---|
forks | How much can the controller do at once? | Ceiling on workers, fleet-wide |
serial | How many hosts may be mid-change at once, and where do we stop? | Batches; failure policy is evaluated at batch boundaries |
throttle | How many hosts may run this task at once? | Lowers concurrency for the throttled scope only |
run_once | Should one host do this on behalf of the batch? | One execution per batch; see the delegation part |
The two substitutions that go wrong:
run_once where throttle: 1 was meant. Each host needs to register
itself with a catalogue, and the catalogue tolerates one writer. run_once
registers one host and leaves 399 unregistered — a correctness bug, and a
quiet one, because the task reports ok.
throttle: 1 where serial: 1 was meant. The intent was “change one
host at a time, and stop if one fails”. throttle: 1 gives the first half
and not the second: with the default failure handling, other hosts continue
after one fails, and there is no batch boundary at which
max_fail_percentage is evaluated.
Knowledge check
Knowledge check · 4 questions
Q1. A play runs with forks: 1 and a task carrying throttle: 4. Four hosts each take two seconds on that task. How long does the task take?
Q2. A block checks out a licence seat, runs a reconfiguration with it, then releases the seat. The licence pool has one seat. Where should throttle: 1 go?
Q3. Which statements correctly distinguish throttle from serial and run_once? Select all that apply.
Q4. Adding throttle: 1 to every task in a play is a reasonable substitute for serial: 1 when you want to change one host at a time and stop on the first failure.
Passing score: 75%. Answers are checked in this browser.