Ansible loop_control: Control Loop Output, Labels & Pauses (Guide)
By Luca Berton · Published 2026-04-03 · Category: installation
How to use Ansible loop_control to customize loop behavior. Set labels, limit output with loop_var, add pauses between iterations, index tracking.
Ansible's loop_control directive gives you fine-grained control over how loops behave — from customizing output labels to adding delays between iterations and tracking loop indices.
Basic loop_control Usage
- name: Install packages with custom label
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- nginx
- postgresql
- redis
loop_control:
label: "{{ item }}"
See also: Ansible Handlers: Trigger Tasks on Change (Complete Guide)
loop_control: label
By default, Ansible prints the entire loop item in output. With complex data structures, this creates unreadable output. Use label to show only what matters:
- name: Create users with clean output
ansible.builtin.user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
groups: "{{ item.groups }}"
loop:
- { name: alice, uid: 1001, groups: admin, shell: /bin/bash, comment: "Alice Admin" }
- { name: bob, uid: 1002, groups: developers, shell: /bin/bash, comment: "Bob Dev" }
loop_control:
label: "{{ item.name }}"
Without label: changed: [host] => (item={'name': 'alice', 'uid': 1001, 'groups': 'admin', ...})
With label: changed: [host] => (item=alice)
loop_control: pause
Add a delay (in seconds) between loop iterations:
- name: Restart services one at a time with 10s gap
ansible.builtin.systemd:
name: "{{ item }}"
state: restarted
loop:
- nginx
- app-backend
- app-worker
loop_control:
pause: 10
This is essential for rolling restarts to avoid downtime.
See also: Ansible Troubleshooting: Fix Jinja2 Syntax & Inventory Errors
loop_control: index_var
Track the current loop index (0-based):
- name: Create numbered config files
ansible.builtin.template:
src: worker.conf.j2
dest: "/etc/workers/worker-{{ my_idx }}.conf"
loop:
- { port: 8001 }
- { port: 8002 }
- { port: 8003 }
loop_control:
index_var: my_idx
loop_control: loop_var
Rename the loop variable (essential for nested loops):
- name: Outer loop
ansible.builtin.include_tasks: inner.yml
loop:
- webservers
- dbservers
loop_control:
loop_var: server_group
# inner.yml
- name: Inner loop
ansible.builtin.debug:
msg: "Processing {{ server_group }} - {{ inner_item }}"
loop:
- task1
- task2
loop_control:
loop_var: inner_item
Without loop_var, nested loops would both try to use item, causing conflicts.
See also: Ansible troubleshooting - Error no-prompting
loop_control: extended
Get extra loop information like first, last, length, index0, index:
- name: Process items with position awareness
ansible.builtin.debug:
msg: >
Item {{ ansible_loop.index }} of {{ ansible_loop.length }}:
{{ item }}
{% if ansible_loop.first %}(FIRST){% endif %}
{% if ansible_loop.last %}(LAST){% endif %}
loop:
- alpha
- beta
- gamma
loop_control:
extended: true
Extended attributes available:
• ansible_loop.index — 1-based position
• ansible_loop.index0 — 0-based position
• ansible_loop.first — true if first item
• ansible_loop.last — true if last item
• ansible_loop.length — total items
• ansible_loop.revindex — reverse index (1-based)
• ansible_loop.revindex0 — reverse index (0-based)
• ansible_loop.previtem — previous item
• ansible_loop.nextitem — next item
loop_control: extended_allitems
By default, ansible_loop.allitems contains all loop items. Set extended_allitems: false to save memory with large loops:
- name: Process large list efficiently
ansible.builtin.command: "process {{ item }}"
loop: "{{ large_list }}"
loop_control:
extended: true
extended_allitems: false
Complete Example: Rolling Deployment
- name: Rolling deployment across app servers
block:
- name: Remove from load balancer
ansible.builtin.uri:
url: "http://lb.example.com/api/servers/{{ item.name }}/disable"
method: POST
- name: Deploy new version
ansible.builtin.command: /opt/deploy.sh
delegate_to: "{{ item.host }}"
- name: Add back to load balancer
ansible.builtin.uri:
url: "http://lb.example.com/api/servers/{{ item.name }}/enable"
method: POST
loop:
- { name: app1, host: 10.0.1.1 }
- { name: app2, host: 10.0.1.2 }
- { name: app3, host: 10.0.1.3 }
loop_control:
label: "{{ item.name }}"
pause: 30
extended: true
## FAQ
### What's the difference between index_var and ansible_loop.index?
`index_var` is 0-based and requires no `extended: true`. `ansible_loop.index` is 1-based and requires `extended: true`. Use `extended` for the richer set of variables.
### How do I skip items in a loop?
Use `when` with loop variables: `when: item != 'skip_me'` or `when: ansible_loop.index > 2`.
label — Clean Loop Output
# Without label: dumps entire dict per iteration
# With label: shows only what you specify
- name: Create users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
loop:
- { name: alice, groups: sudo, password: "secret1" }
- { name: bob, groups: docker, password: "secret2" }
loop_control:
label: "{{ item.name }}" # Shows "alice", "bob" instead of full dict
pause — Delay Between Iterations
- name: Rolling restart
ansible.builtin.service:
name: myapp
state: restarted
loop: "{{ groups['webservers'] }}"
loop_control:
pause: 10 # Wait 10 seconds between each server
delegate_to: "{{ item }}"
index_var — Access Loop Index
- name: Show index
debug:
msg: "{{ idx }}: {{ item }}"
loop: [apple, banana, cherry]
loop_control:
index_var: idx
# 0: apple, 1: banana, 2: cherry
loop_var — Rename Loop Variable
# Essential for nested loops (include_tasks)
- name: Process servers
ansible.builtin.include_tasks: configure.yml
loop: "{{ servers }}"
loop_control:
loop_var: server # Inner tasks use 'server' instead of 'item'
# configure.yml
- name: Install packages on {{ server.name }}
apt:
name: "{{ item }}"
loop: "{{ server.packages }}" # 'item' available for inner loop
become: true
extended — Extra Loop Info
- debug:
msg: |
Item: {{ item }}
Index: {{ ansible_loop.index }} (1-based)
Index0: {{ ansible_loop.index0 }} (0-based)
First: {{ ansible_loop.first }}
Last: {{ ansible_loop.last }}
Length: {{ ansible_loop.length }}
Remaining: {{ ansible_loop.revindex }}
loop: [a, b, c]
loop_control:
extended: true
extended_allitems
# Also include ansible_loop.allitems (full list)
- debug:
msg: "Processing {{ item }} of {{ ansible_loop.allitems | join(', ') }}"
loop: [web, api, db]
loop_control:
extended: true
extended_allitems: true
Practical Examples
Deploy with progress indicator
- name: "Deploying [{{ ansible_loop.index }}/{{ ansible_loop.length }}]: {{ item.name }}"
ansible.builtin.command: "./deploy.sh {{ item.name }}"
loop: "{{ services }}"
loop_control:
extended: true
label: "{{ item.name }}"
First/last item handling
- name: Configure cluster node
template:
src: node.conf.j2
dest: "/etc/cluster/node-{{ item }}.conf"
loop: "{{ cluster_nodes }}"
loop_control:
extended: true
vars:
is_primary: "{{ ansible_loop.first }}"
All loop_control Options
| Option | Description |
|--------|-------------|
| label | Custom output label |
| pause | Seconds between iterations |
| index_var | Variable name for index (0-based) |
| loop_var | Rename item variable |
| extended | Enable ansible_loop object |
| extended_allitems | Include full list in ansible_loop |
FAQ
Why use loop_var?
When using include_tasks with a loop, the inner tasks also use item. Renaming the outer loop variable avoids conflicts.
Does pause work with async tasks?
No — pause is synchronous. For async, use async + poll instead.
How do I skip certain loop items?
- debug: msg="{{ item }}"
loop: [1, 2, 3, 4, 5]
when: item > 2
label (Clean Output)
# Without label — dumps entire object per iteration
- user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
loop:
- { name: alice, groups: [sudo, dev], uid: 1001 }
- { name: bob, groups: [dev], uid: 1002 }
loop_control:
label: "{{ item.name }}"
# Output: "ok: [web1] => (item=alice)" instead of full dict
pause (Delay Between Iterations)
# Wait 5 seconds between each restart (rolling restart)
- service:
name: "{{ item }}"
state: restarted
loop: "{{ app_services }}"
loop_control:
pause: 5
become: true
index_var (Loop Counter)
- debug:
msg: "{{ idx }}: {{ item }}"
loop: [alpha, bravo, charlie]
loop_control:
index_var: idx
# 0: alpha, 1: bravo, 2: charlie
loop_var (Custom Variable Name)
# Avoid collision in nested loops / includes
- include_tasks: setup-user.yml
loop: "{{ users }}"
loop_control:
loop_var: outer_user
# setup-user.yml:
- file:
path: "/home/{{ outer_user.name }}/{{ inner_dir }}"
state: directory
loop: [.ssh, .config, bin]
loop_control:
loop_var: inner_dir
extended (Loop Metadata)
- debug:
msg: |
Item: {{ item }}
Index: {{ ansible_loop.index }} (1-based)
Index0: {{ ansible_loop.index0 }} (0-based)
First: {{ ansible_loop.first }}
Last: {{ ansible_loop.last }}
Length: {{ ansible_loop.length }}
Revindex: {{ ansible_loop.revindex }}
loop: [a, b, c]
loop_control:
extended: true
extended_allitems
# Access all items from within the loop
- debug:
msg: "Processing {{ item }} of {{ ansible_loop.allitems | join(', ') }}"
loop: [web, db, cache]
loop_control:
extended: true
extended_allitems: true
Combined Options
- docker_container:
name: "{{ svc.name }}"
image: "{{ svc.image }}"
state: started
loop: "{{ services }}"
loop_control:
loop_var: svc
label: "{{ svc.name }}"
pause: 3
index_var: svc_idx
extended: true
Practical: Rolling Deploy
- name: Deploy to servers one at a time
include_tasks: deploy-single.yml
loop: "{{ groups['webservers'] }}"
loop_control:
loop_var: target_host
pause: 30 # 30s between each server
label: "{{ target_host }}"
FAQ
Why use label?
Without it, Ansible prints the entire loop item (which can be a huge dict). label keeps output readable.
Does pause affect total runtime?
Yes — pause: 5 with 10 items adds ~45 seconds (pauses between, not after last).
When do I need loop_var?
When including tasks that also use loops — without unique loop_var names, the inner item overwrites the outer one.
Related Articles
• rendering files with Ansible template • Ansible conditional patterns • the Ansible Nginx reference • iterating tasks with Ansible loopsCategory: installation