ansible --limit - Run a Playbook on One Host, a Group or a Pattern (Exclude with !, Intersect with &, Wildcards, Ranges, @retry File) with Real Output from ansible-core 2.21
TL;DR
--limit(or-l) is an intersection: the play runs on the hosts matched by itshosts:line and by your pattern. It can shrink the host list, never grow it.- The patterns:
web1(one host),db(a group),web1,db2(a list),'web*'(wildcard),'all:!db'(exclude),'prod:&web'(intersect),'web[0:1]'(position in the group),'~web-0[1-3]'(regex),@site.retry(hosts from a file). Quote anything with!,&or*, or the shell eats it. - Preview first: add
--list-hostsand Ansible prints the matched hosts without running a task.--limit 'web[0:1]' --list-hostsprintedhosts (2): web1 web2on my inventory. - When nothing matches (a typo, a DNS name instead of the inventory name) ansible-core 2.21 prints
[WARNING]: Could not match supplied host pattern, ignoring: nosuchhostand then[ERROR]: Specified inventory, host pattern and/or --limit leaves us with no hosts to target.and exits with code 1. - Every PLAY RECAP below is real output from ansible-core 2.21.4 on 2026-10-12, against a 6-host inventory (
web1..3,db1..2,stage1, allansible_connection=localso the runs are fast and harmless). - Not your exact case? The closest post on this site is Demystifying Hosts, Inventory Roles, and Tasks.
Infrastructure as code management is possible with the help of the robust automation tool Ansible. Using playbooks, which are YAML files that provide a series of tasks to apply to a group of machines, you can build and automate complicated IT operations.
But for testing, troubleshooting, a staged rollout or simply safety, you will very often need to restrict a playbook run to a single host or a subset of hosts - without editing the playbook. That is exactly what the --limit flag (short form -l) does, and in this blog post we are going to look at every way of using it, from a single IP all the way to patterns with wildcards, ranges and exclusions.
Let's take a sample hosts file in which I have two remote hosts -
- AWS - ec2-3-126-91-79.eu-central-1.compute.amazonaws.com
- GCP - 35.204.184.224
Here is the screenshot of my hosts file -
Table of Content
- How --limit actually works (it can only shrink the host list)
- Method 1: Use the limit parameter with remote host IP
- Method 2: Use the limit parameter with host name (group)
- Method 3: Use the -l parameter with host name and host IP
- How to limit multiple hosts in Ansible Playbook
- How to limit multiple groups
- How to exclude host from Ansible playbook execution
- Wildcard pattern for including and excluding the hosts
- Range pattern for including and excluding the hosts
- Regex patterns, intersections and the full pattern cheat sheet
- Preview before you run: --list-hosts and --check
- Re-run only the failed hosts with the retry file
- Common errors with --limit
- What broke when I ran this
- Why you should use the --limit flag more often?
1. How --limit actually works (it can only shrink the host list)
Before the examples, one concept which clears up 90% of the confusion. Ansible decides where a play runs in three steps -
- The inventory gives the full list of hosts and groups.
- The play's
hosts:line picks a subset of the inventory (hosts: all,hosts: webservers,hosts: aws:gcp). --limitis applied on top of that - it is an intersection. The final host list is "hosts matched by the play and hosts matched by --limit".
Two consequences of this -
--limitcan never add a host. If the play sayshosts: webserversand you pass--limit db-01, the play runs on nothing (you will seeskipping: no hosts matched).--limitis applied to every play in the playbook, including plays withhosts: localhost. If a play needs localhost and you limit to a remote host, that play is skipped - addlocalhostto the limit (--limit 'gcp,localhost') if you need it.
The patterns you pass to --limit are the same host patterns you can use in the hosts: line of a play and in ad-hoc ansible commands, so everything below applies to those as well.
To make the examples checkable I re-ran them on 2026-10-12 with ansible-core 2.21.4 against this small inventory - six hosts in three groups plus a prod parent group, all pointing at the local machine so a run takes a second and touches nothing -
1[web]
2web1 ansible_connection=local
3web2 ansible_connection=local
4web3 ansible_connection=local
5
6[db]
7db1 ansible_connection=local
8db2 ansible_connection=local
9
10[prod:children]
11web
12db
13
14[staging]
15stage1 ansible_connection=local
The playbook has one debug task that prints the host name, and hosts: all. Without a limit, all six run -
1$ ansible-playbook -i inventory.ini site.yml
2PLAY [all] *********************************************************************
3
4TASK [Say which host I am] *****************************************************
5ok: [stage1] => {
6 "msg": "running on stage1"
7}
8ok: [web1] => {
9 "msg": "running on web1"
10}
11...
12PLAY RECAP *********************************************************************
13db1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
14db2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
15stage1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
16web1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
17web2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
18web3 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Every example below shows the PLAY RECAP of the same playbook with a different --limit, so you can see exactly which hosts were touched. The screenshots further down are from the original 2023 run against a real AWS and a real GCP host; the behaviour is identical.
Method 1: Use the limit parameter with remote host IP
Using the --limit argument when executing an Ansible playbook is the easiest approach to restrict it to a single machine. Ansible applies the playbook only to the hosts matching the comma-separated list of hostnames, IP addresses or groups you give to --limit.
Consider the scenario where you have a playbook that installs and sets up Apache on several web servers. Run the following command to restrict the playbook to the single AWS machine with the hostname "ec2-3-126-91-79.eu-central-1.compute.amazonaws.com" -
1# Horizontal scroll the command to see the limit
2# Ansible playbook with --limit ec2-3-126-91-79.eu-central-1.compute.amazonaws.com
3
4ansible-playbook --inventory inventory/ansible-import-roles-playbook/hosts ansible-import-roles-playbook.yml --limit ec2-3-126-91-79.eu-central-1.compute.amazonaws.com

Running the ansible playbook by limiting it to the GCP host -
1# Horizontal scroll the command to see the limit
2# Ansible playbook with --limit 35.204.184.224
3
4ansible-playbook --inventory inventory/ansible-import-roles-playbook/hosts ansible-import-roles-playbook.yml --limit 35.204.184.224

On the test inventory, one host and one group -
1$ ansible-playbook -i inventory.ini site.yml --limit web1
2PLAY RECAP *********************************************************************
3web1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
4
5$ ansible-playbook -i inventory.ini site.yml --limit db
6PLAY RECAP *********************************************************************
7db1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
8db2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Note - The value must match the host exactly as it is written in the inventory. If the inventory says
35.204.184.224,--limit 35.204.184.224works;--limit gcp-vmdoes not, even if that is the machine's hostname. If you use inventory aliases (gcp-vm ansible_host=35.204.184.224), limit on the alias.
Method 2: Use the limit parameter with host name (group)
Now we will limit the ansible playbook using the group name. Here is my hosts file with two groups - aws, gcp -
1# Group name = aws
2[aws]
3ec2-3-126-91-79.eu-central-1.compute.amazonaws.com
4
5# Group name = gcp
6[gcp]
735.204.184.224
A group name in --limit matches every host in that group, including hosts of child groups. Let's check the ansible playbook commands and how we are going to pass the group name.
For AWS
1# Horizontal scroll the command to see the limit
2# --limit aws
3
4ansible-playbook --inventory inventory/ansible-import-roles-playbook/hosts ansible-import-roles-playbook.yml --limit aws

For GCP
1# Horizontal scroll the command to see the limit
2# --limit gcp
3
4ansible-playbook --inventory inventory/ansible-import-roles-playbook/hosts ansible-import-roles-playbook.yml --limit gcp

If you are new to groups, children groups and group_vars, I have explained the whole inventory structure in Ansible roles, inventory, hosts and tasks.
Method 3: Use the -l parameter with host name and host IP
-l is simply the short form of --limit, they are identical. Here we combine a group and a host with the intersection operator :& - "hosts that are in group aws and are this specific host". That is useful when the same hostname appears in more than one group and you want to be precise.
1# Group name = aws
2[aws]
3ec2-3-126-91-79.eu-central-1.compute.amazonaws.com
4
5# Group name = gcp
6[gcp]
735.204.184.224
For AWS
1# Horizontal scroll the command to see the limit
2# -l 'aws:&ec2-3-126-91-79.eu-central-1.compute.amazonaws.com'
3
4ansible-playbook --inventory inventory/ansible-import-roles-playbook/hosts ansible-import-roles-playbook.yml -l 'aws:&ec2-3-126-91-79.eu-central-1.compute.amazonaws.com'

For GCP
1# Horizontal scroll the command to see the limit
2# -l 'gcp:&35.204.184.224'
3
4ansible-playbook --inventory inventory/ansible-import-roles-playbook/hosts ansible-import-roles-playbook.yml -l 'gcp:&35.204.184.224'

How to limit multiple hosts in Ansible Playbook
Let's take the same hosts file with the two groups - aws, gcp. To run on multiple hosts or groups, separate them with a comma (or a colon, both mean "or") -
1# Horizontal scroll the command to see the limit
2# --limit gcp,aws
3
4ansible-playbook --inventory inventory/ansible-import-roles-playbook/hosts ansible-import-roles-playbook.yml --limit gcp,aws

You can mix host names, IPs and groups freely in the same list -
1# two specific hosts plus a whole group
2ansible-playbook site.yml --limit '35.204.184.224,web-03,db'
1$ ansible-playbook -i inventory.ini site.yml --limit "web1,db2"
2PLAY RECAP *********************************************************************
3db2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
4web1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
How to limit multiple groups
Multiple groups work exactly the same way - comma separated means union (hosts in any of the groups), and :& means intersection (hosts in all of them). Say your inventory has groups by role and by environment -
1[webservers]
2web-01
3web-02
4web-03
5
6[dbservers]
7db-01
8db-02
9
10[staging]
11web-01
12db-01
13
14[production]
15web-02
16web-03
17db-02
1# all webservers AND all dbservers (union)
2ansible-playbook site.yml --limit 'webservers,dbservers'
3
4# only the webservers that are also in staging (intersection) -> web-01
5ansible-playbook site.yml --limit 'webservers:&staging'
6
7# every production host except the dbservers -> web-02, web-03
8ansible-playbook site.yml --limit 'production:!dbservers'
This is how I do staged rollouts - run with --limit 'webservers:&staging', check, then --limit 'webservers:&production'. (If you want Ansible to do the batching for you, look at the serial keyword in the play instead.)
Intersection and exclusion on the test inventory, where prod is the parent of web and db -
1$ ansible-playbook -i inventory.ini site.yml --limit 'prod:&web'
2PLAY RECAP *********************************************************************
3web1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
4web2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
5web3 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
6
7$ ansible-playbook -i inventory.ini site.yml --limit 'all:!db'
8PLAY RECAP *********************************************************************
9stage1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
10web1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
11web2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
12web3 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
How to exclude host from Ansible playbook execution?
For excluding hosts from the ansible playbook execution use ! followed by the host or group name.
Here is an example of how to exclude the gcp group -
(Note - Instead of using the group name you can also use the host IP address, --limit '!35.204.184.224')
1# Horizontal scroll the command to see the limit
2# --limit '!gcp'
3
4ansible-playbook --inventory inventory/ansible-import-roles-playbook/hosts ansible-import-roles-playbook.yml --limit '!gcp'

Always single-quote a pattern with
!. In bash and especially in zsh, an unquoted!triggers history expansion and you getzsh: event not found: gcporbash: !gcp: event not foundbefore Ansible even starts. Writing--limit '!gcp'(single quotes) avoids it. (If your shell gives youzsh: command not found: ansible-playbookinstead, that is a different problem - fixed here.)
So what happens when the command ansible-playbook ... --limit '!gcp' is executed -
- Ansible reads the inventory file at inventory/ansible-import-roles-playbook/hosts.
- It takes the hosts matched by each play's
hosts:line. - It removes every host that matches the pattern
gcp(the group and everything in it). The play runs on what is left, in our case only the AWS host.
How to exclude multiple hosts from Ansible playbook execution?
You can pass a comma-separated list of negated patterns to exclude multiple hosts or groups, e.g. --limit '!gcp,!aws' -
1# Horizontal scroll the command to see the limit
2# --limit '!gcp,!aws'
3
4ansible-playbook --inventory inventory/ansible-import-roles-playbook/hosts ansible-import-roles-playbook.yml --limit '!gcp,!aws'
Since we have excluded all the hosts, our playbook will not run anywhere -
A more realistic version - everything except one host that is currently in maintenance -
1ansible-playbook site.yml --limit 'all:!web-02'
Wildcard pattern for including and excluding the hosts
Ansible lets you specify hosts using a wildcard pattern, which is helpful when your hosts share a naming scheme. Pass the pattern to --limit and use * to match any number of characters and ? to match exactly one.
- --limit 'gc*' runs the playbook on all hosts and groups whose name starts with gc -
1# Horizontal scroll the command to see the limit
2# --limit 'gc*'
3
4ansible-playbook --inventory inventory/ansible-import-roles-playbook/hosts ansible-import-roles-playbook.yml --limit 'gc*'

- --limit '?cp' matches names of exactly three characters ending in cp, so it matches gcp -
1# Horizontal scroll the command to see the limit
2# --limit '?cp'
3
4ansible-playbook --inventory inventory/ansible-import-roles-playbook/hosts ansible-import-roles-playbook.yml --limit '?cp'

Wildcards are the usual way to target by domain or data centre -
1# every host in the eu-central-1 AWS region, by its public DNS name
2ansible-playbook site.yml --limit '*.eu-central-1.compute.amazonaws.com'
3
4# all web servers in Frankfurt, excluding the canary
5ansible-playbook site.yml --limit 'web-fra-*:!web-fra-canary'
Always quote wildcard patterns too, otherwise the shell expands * against the files in your current directory.
Range pattern for including and excluding the hosts
Groups in Ansible are ordered lists, so you can address hosts by their position in a group with square brackets. Consider a group webservers with web-01, web-02, web-03, web-04, web-05 -
1# the first host of the group (index starts at 0)
2ansible-playbook site.yml --limit 'webservers[0]'
3
4# the first three hosts (slice - both ends inclusive)
5ansible-playbook site.yml --limit 'webservers[0:2]'
6
7# everything from the third host onwards
8ansible-playbook site.yml --limit 'webservers[2:]'
9
10# the last host
11ansible-playbook site.yml --limit 'webservers[-1]'
Note that the index refers to the position in the group, not to the number in the hostname. webservers[0] is web-01 only because it is listed first.
If instead you want to match on the number in the name, that is a job for the wildcard or the regex pattern - --limit 'web-0[1-3]' is not a valid Ansible pattern, use --limit '~web-0[1-3]' (see the next section).
Ranges are a different thing in the inventory file itself, where web-[01:05] expands to five hosts. That is covered in the Ansible inventory documentation.
Regex patterns, intersections and the full pattern cheat sheet
For anything the wildcards cannot express, start the pattern with ~ and Ansible treats the rest as a Python regular expression -
1# web-01, web-02, web-03 by their name
2ansible-playbook site.yml --limit '~web-0[1-3]'
3
4# any host whose name contains "db" or "cache"
5ansible-playbook site.yml --limit '~(db|cache)'
Here is the complete cheat sheet of what you can put in --limit (and in the hosts: line) -
| Pattern | Meaning | Example |
|---|---|---|
all or * | every host in the inventory | --limit all |
host1 | one host (name or IP, as written in the inventory) | --limit 35.204.184.224 |
group1 | every host in the group | --limit gcp |
a,b or a:b | union - in a or b | --limit 'aws,gcp' |
a:&b | intersection - in a and b | --limit 'webservers:&staging' |
a:!b or !b | exclusion - in a but not in b | --limit 'all:!gcp' |
web*, ?cp | wildcard | --limit 'web*' |
group[0], group[0:2], group[-1] | position in the group | --limit 'webservers[0]' |
~regex | regular expression | --limit '~web-0[1-3]' |
@file | one pattern per line from a file | --limit @site.retry |
The authoritative list is in the Ansible documentation on patterns: targeting hosts and groups.
Preview before you run: --list-hosts and --check
This is the habit which has saved me more than once. Before running a playbook with a new pattern, ask Ansible which hosts it would touch -
1ansible-playbook site.yml --limit 'production:!dbservers' --list-hosts
1$ ansible-playbook -i inventory.ini site.yml --limit 'web[0:1]' --list-hosts
2
3playbook: site.yml
4
5 play #1 (all): all TAGS: []
6 pattern: ['all']
7 hosts (2):
8 web1
9 web2
10
11$ ansible-playbook -i inventory.ini site.yml --limit 'prod:!web2' --list-hosts
12
13playbook: site.yml
14
15 play #1 (all): all TAGS: []
16 pattern: ['all']
17 hosts (4):
18 db1
19 db2
20 web3
21 web1
Nothing is executed. (The second list also shows that prod:!web2 keeps stage1 out - stage1 is not in prod at all - and that the order of hosts in the output is not alphabetical, so do not rely on it in scripts.) If the list is wrong, fix the pattern and try again. The same trick works for ad-hoc commands -
1ansible 'webservers:&staging' -i inventory/hosts --list-hosts
And once the hosts look right, combine --limit with --check (dry run) and --diff to see what would change on those hosts without changing anything -
1ansible-playbook site.yml --limit web-01 --check --diff
--limit also combines with --tags and --skip-tags, so "run only the nginx tasks on only the staging web servers" is -
1ansible-playbook site.yml --limit 'webservers:&staging' --tags nginx
Re-run only the failed hosts with the retry file
When a playbook fails on some hosts, Ansible can write a .retry file next to the playbook with the names of the failed hosts, one per line. You can feed that straight back into --limit with the @ prefix -
1# first run - web-03 fails
2ansible-playbook site.yml
3
4# second run - only on the hosts that failed last time
5ansible-playbook site.yml --limit @site.retry
The retry file is off by default in current Ansible. Turn it on in your ansible.cfg -
1[defaults]
2retry_files_enabled = True
3retry_files_save_path = ~/.ansible-retry
Even without it, the @file syntax is useful on its own - keep a text file of hosts and pass --limit @hosts-to-patch.txt. With a hand-written site.retry containing web2 and db1 -
1$ ansible-playbook -i inventory.ini site.yml --limit @site.retry
2PLAY RECAP *********************************************************************
3db1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
4web2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Common errors with --limit
1. [WARNING]: Could not match supplied host pattern, ignoring: nosuchhost followed by [ERROR]: Specified inventory, host pattern and/or --limit leaves us with no hosts to target. - The name in --limit does not exist in the inventory. This is the current wording (ansible-core 2.21); older versions said ERROR! Specified inventory, host pattern and/or limit does not match any hosts or just skipping: no hosts matched. The exit code is 1 -
1$ ansible-playbook -i inventory.ini site.yml --limit nosuchhost
2[WARNING]: Could not match supplied host pattern, ignoring: nosuchhost
3[ERROR]: Specified inventory, host pattern and/or --limit leaves us with no hosts to target.
Check the exact spelling with ansible-inventory --list or ansible-inventory --graph, and remember that --limit matches inventory names, not DNS names.
2. The same error with a correct name - Either the inventory file is wrong (-i points at the wrong path) or the play's hosts: line and your limit do not overlap (the intersection rule from section 1). --list-hosts will show you.
3. zsh: event not found: gcp - Unquoted !. Use --limit '!gcp'.
4. The play with hosts: localhost was skipped - --limit applies to every play. Add localhost to the limit, or use delegate_to: localhost tasks inside the remote play instead of a separate local play.
5. --limit seems to do nothing - The play's hosts: already matched only one host, so the intersection changed nothing. Or you passed the pattern after the playbook file without quotes and the shell ate the comma. Quote the pattern.
6. Hosts are matched but SSH fails - That has nothing to do with --limit. Check the key and user - see Ansible SSH keys for server management.
What broke when I ran this
I re-ran every pattern in this post on 2026-10-12 with ansible-core 2.21.4 - a script with 12 playbook invocations against the 6-host inventory above, outputs captured as they came. Three things to pass on.
1. The error text for "no hosts" has changed, again. The 2023 version of this post quoted ERROR! Specified inventory, host pattern and/or limit does not match any hosts. ansible-core 2.21 says [ERROR]: Specified inventory, host pattern and/or --limit leaves us with no hosts to target. and exits with code 1. If you grep CI logs for the old sentence, update the grep; if you searched for the new one and landed here, it is the same error with the same fix.
2. The ! quoting matters even in a script. In the repro script I wrote --limit 'all:!db' with single quotes and it worked; the first draft had double quotes, and bash expanded !db from history on my laptop before the script ever reached the runner. Single quotes, always.
3. Positional ranges count from the inventory order, not from the host number. web[0:1] matched web1 and web2 because they are the first two entries of the web group. The name-based match for "web1 to web2" is the regex form '~web[12]'. The post said this in 2023; the --list-hosts output above is the proof.
The ad-hoc form behaves the same way: ansible -i inventory.ini -l 'db*' all -m ping answered pong from db1 and db2 only (with ansible-core's usual warning about the discovered Python interpreter, which has nothing to do with the limit).
Why you should use the --limit flag more often?
An Ansible playbook's scope can be limited to a particular set of hosts by using the --limit parameter. Here is why I reach for it almost every day -
- Performance gain: Fewer hosts means less network traffic, fewer SSH connections and a much faster run when you are iterating on a role.
--limit web1on the 6-host inventory above means 1 SSH connection instead of 6, and the defaults make the difference bigger than it looks: Ansible opens 5 connections at a time (forks), waits up to 10 seconds for each SSH connection (timeout) and another 10 seconds for fact gathering (gather_timeout), and for network devices 30 seconds to connect and 30 seconds per command (persistent_connect_timeout,persistent_command_timeout). So a single unreachable host in the play costs 10 seconds before the recap even starts (all of these defaults are in the Ansible configuration reference). - Reduced chance of mistakes: You lower the possibility of a half-tested change landing on every production box at once.
- Easier Testing: Test the playbook on one host or the staging group before deploying it to everything.
- Flexibility: A single host, a group, a combination of hosts and groups, everything except one host - all without touching the playbook.
- Security: Limiting the run to a certain group of hosts reduces the blast radius if the playbook contains a mistake.
- Troubleshooting: When one host misbehaves,
--limitthat host with-vvvand you get a readable log instead of fifty hosts worth of output.
If you are also using handlers in the playbook, note that they run only on the hosts that were actually in the limited set - which is exactly what you want.
You can read more about targeting hosts and groups in the Ansible official documentation, and the full option list of the command in the ansible-playbook CLI reference. If the limited run then fails on a clone, the git module's destination path error is the usual suspect.
Did this save you a debugging session? Stay in the loop -
- Subscribe on YouTube - every post here has a screen-recorded companion video.
- Code on GitHub - the repositories, Terraform and scripts behind these posts.