AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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 templateAnsible conditional patternsthe Ansible Nginx referenceiterating tasks with Ansible loops

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home