Skip to content

When Not to Loop


This closing chapter is a caution, not a technique: now that you can loop over almost anything, it’s worth knowing exactly when not to — because looping a module N times can be doing real, unnecessary work a single call could have handled instead.

Some Modules Accept A List Natively

Many modules — package managers especially — accept a list directly as one of their own parameters, handling the entire batch internally in a single operation, rather than needing you to loop: the module once per item at all.

Why This Matters: Performance

Looping a module N times means N separate invocations of that module against the host — N separate rounds of whatever overhead that module carries with it (locking, cache reads, connection setup). A module that accepts a list natively does the whole batch in one invocation instead. That difference compounds fast: N items across M hosts, looped, is N × M separate operations; the same work through a native list parameter is just M operations, one per host, each handling every item at once.

An Example: Package Installation

# Inefficient: loops the module three separate times
- name: Install packages one at a time
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - curl
    - git
# Efficient: one invocation, the whole list handled natively
- name: Install packages in one go
  ansible.builtin.apt:
    name:
      - nginx
      - curl
      - git
    state: present

Same end result — all three packages installed — but the second version runs apt once, covering all three, instead of three completely separate invocations each carrying their own overhead.

How To Tell If A Module Supports This

Check the module’s own documentation — a parameter’s accepted type is listed explicitly, and if it accepts a list, native batch handling is usually available. When you’re not sure, checking the docs before defaulting to loop: out of habit is worth the extra minute.

When Looping Is Still The Right Choice

Looping remains exactly correct whenever each item genuinely needs different configuration beyond a single shared list value — creating several users, each with their own distinct role and home directory, for instance, isn’t something a single native-list call can express, since there’s no one shared parameter that varies per item in that way. It’s also simply the only option for modules that don’t support list input for the relevant parameter at all.

Best Practices

  • Check a module’s documentation for native list support before reaching for loop: by default — it’s an easy habit to skip, and an easy one worth building.
  • Prefer a native list parameter for uniform, same-operation-different-target work — installing several packages with the same state, for instance.
  • Keep using loop: when each iteration genuinely needs distinct, per-item configuration, or when the module you’re using simply doesn’t support a list for what you need at all.

Closing Thoughts For This Section

Ten chapters, starting from a single flat list and {{ item }}, and ending here — dictionaries as loop sources, the real shape of a registered looped result, dict2items finally put to use, loop_control for readable and throttled output, the nested-loop naming collision and its fix, filters feeding directly into loop:’s own source, genuine retry logic with until, the full with_X family translated into modern form, and now, knowing when a loop isn’t even the right tool at all. The next section moves to blocks and handlers — grouping tasks together for shared error handling, and finally paying off the “you’ll meet a more elegant way to do this later” note left dangling back in the tests and conditionals section’s change-detection pattern.

Last updated on