--limit is the most-used safety control in Ansible, and it is
misunderstood in one specific way that matters.
--limit intersects with the play’s hosts: line. It does not
replace it.
The effective host list is the intersection of the two. A --limit can
only ever make a play target fewer hosts than its hosts: line already
selects — never more, never different.
That is a strong guarantee and it is the reason --limit is the right
habitual way to narrow a run. It is also why a --limit naming a host
the play never targeted quietly does nothing.
The intersection, demonstrated
Every example uses one playbook whose single play is hosts: web,
against the ten-host inventory from lesson 2.
Read-only / Safeno limit - the play's own pattern— Real output from ansible-core 2.21.3. Four hosts, from the play's hosts: web.
playbook: site.yml
play #1 (web): Configure the web tier TAGS: []
pattern: ['web']
hosts (4):
web01.example.com
web02.example.com
web03.example.com
web04.example.com
Read-only / Safe--limit narrows to one host— Real output from ansible-core 2.21.3. Note the pattern line still reads ['web'] - the play's own target is unchanged, and the host list below it is the intersection.
play #1 (web): Configure the web tier TAGS: []
pattern: ['web']
hosts (1):
web01.example.com
Read-only / Safe--limit all does not widen anything— Real output from ansible-core 2.21.3. The inventory has ten hosts. The play still targets four, because --limit all intersects with web rather than replacing it.
$ ansible-playbook -i inventory/hosts.yml site.yml --limit all --list-hosts
play #1 (web): Configure the web tier TAGS: []
pattern: ['web']
hosts (4):
web04.example.com
web02.example.com
web03.example.com
web01.example.com
That third case is the guarantee in action. There is no --limit value
that reaches a host the play was not already going to reach.
The four shapes you will actually use
Configuration changesingle host - the smallest possible change— One host. The first form to reach for when validating anything on real infrastructure.
Configuration changecanary group - a named set that survives inventory changes— Better than naming a host, because the canary group is a deliberate inventory decision rather than a hostname somebody memorised.
Configuration changeregion or environment - a blast-radius boundary that already exists— Intersects with the play's own pattern, so this is the web tier in one region without needing a combined group.
-l is the short form and works identically on ansible,
ansible-playbook and ansible-inventory:
Read-only / Safethe same limit on three commands— Real output from ansible-core 2.21.3, abbreviated. ansible-inventory --list honours --limit; --graph does not, which is the subject of the next lesson.
$ ansible -i inventory/hosts.yml web -l canary --list-hosts
hosts (1):
web01.example.com
Limit files
--limit @file reads the pattern from a file. This is the form that
turns a host list into a reviewable artefact.
Read-only / Safechange-ops-5512.limit— Comments and blank lines are permitted. Group names and operators work exactly as in an inline pattern.
# change ticket OPS-5512 - approved 2026-08-11 by the platform team
# generated from: ansible -i inventory/ 'web:&eu_west' --list-hosts
web01.example.com
web03.example.com
Read-only / Safethe file drives the run— Real output from ansible-core 2.21.3. The comment and the blank line are ignored; two hosts result, still intersected with the play's hosts: web.
play #1 (web): Configure the web tier TAGS: []
pattern: ['web']
hosts (2):
web03.example.com
web01.example.com
Operators are allowed in a limit file too, which is occasionally what you
want and more often a sign the file should have been a plain list:
Read-only / Safea limit file containing a group and an exclusion— Real output from ansible-core 2.21.3. The file contained the two lines us_east and !canary, and the result is the same three hosts as the inline pattern.
play #1 (web): Configure the web tier TAGS: []
pattern: ['web']
hosts (3):
web04.example.com
web03.example.com
web02.example.com
A missing limit file is a hard error rather than an empty limit, which
is the correct behaviour and worth knowing:
Read-only / Safethe file is not there— Real output from ansible-core 2.21.3, exit code 1. A typo in the filename stops the run instead of silently running unlimited - the opposite of what a naive implementation would do.
Add a header comment naming the ticket, the date, and the pattern the list came from, so the file explains itself six months later.
Attach the file to the change record. It is the answer to "which hosts" in a form a reviewer can count and disagree with.
Run against the file rather than the pattern: --limit @change-OPS-5512.limit. The list was frozen at review time, so a host created since then is not in it.
Keep the file with the change record. After an incident it is the only artefact that says what the run was meant to touch, as distinct from what it did touch.
Step four is the one that matters for a dynamic inventory. A pattern
re-resolves at execution time and can produce a different answer from
the one that was reviewed; a limit file cannot. That is the closest a
dynamic estate gets to the property a static inventory has for free.
Knowledge check
Knowledge check · 4 questions
Q1. A play has hosts: web, which resolves to four hosts. The inventory has ten hosts. What does --limit all produce?
Q2. An operator runs the web playbook with --limit db01.example.com, where db01 exists in the inventory. What happens?
Q3. Which are true of a limit file passed as --limit @file? Select all that apply.
Q4. Passing --limit twice on one command line applies only the second one.
Passing score: 75%. Answers are checked in this browser.