AnsibleXXIII · Tags, Blocks and Error HandlingError handling
When a retry makes it worse
What you'll learn
- State how many times retries: N actually executes a task
- Decide whether a failure is transient enough to be worth retrying
- Recognise a retry that multiplies damage rather than absorbing a fault
- Use meta: clear_host_errors correctly, including what it cannot do
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
A retry is a claim: this will probably work if I wait. When the claim is true, a retry loop turns a flaky dependency into a non-event. When it is false, it converts a fast failure into a slow one, or — worse — repeats a change that was not safe to repeat.
The keywords:
$ ansible-doc -t keyword until retries delaydelay:
applies_to:
- Task
- Handler
description: Number of seconds to delay between retries. This setting is only used
in combination with `until`.
priority: 0
template: explicit
type: float
retries:
applies_to:
- Task
- Handler
description: Number of retries before giving up in a `until` loop. This setting
is only used in combination with `until`.
priority: 0
template: explicit
type: int
until:
applies_to:
- Task
- Handler
description: This keyword implies a '`retries` loop' that will go on until
the condition supplied here is met or we hit the `retries` limit.
priority: 0
template: implicit
type: listuntil is the one that turns the loop on. retries and delay do nothing
without it.
How many attempts you actually get
The word “retries” suggests attempts in addition to the first one. Measured on 2.21.3, it is the total.
A task with until: false — a condition that can never be satisfied —
wrapped in a block whose rescue reports ansible_failed_result.attempts:
$ for n in 1 2 3; do ansible-playbook -i inventory.ini r$n.yml; done--- retries: 1 ---
RETRYING lines: 1
retries=1 -> attempts=1
--- retries: 2 ---
RETRYING lines: 2
retries=2 -> attempts=2
--- retries: 3 ---
RETRYING lines: 3
retries=3 -> attempts=3retries: N gives N attempts, not N + 1. And omitting retries while
using until gives you three:
$ ansible-playbook -i inventory.ini rdef.ymldefault retries -> attempts=3One display quirk to expect: the countdown in the retry message reaches zero
before the task gives up, so the last line before the failure reads (0 retries left). That is cosmetic — the attempt count above is the number that
matters.
$ ansible-playbook -i inventory.ini failedwhen.ymlTASK [retries and until] *******************************************************
FAILED - RETRYING: [localhost]: retries and until (2 retries left).
FAILED - RETRYING: [localhost]: retries and until (1 retries left).
ok: [localhost]
TASK [show attempts] ***********************************************************
ok: [localhost] => {
"msg": "attempts=3"
}The attempts field is on the registered result, which makes it available
for a report — “this node needed four attempts to come back” is a useful
thing to log.
The honest retry
A retry is honest when the failure is transient by nature — the thing you are waiting for is genuinely in the process of becoming true.
- name: wait for the service to answer its health endpoint
ansible.builtin.uri:
url: "http://127.0.0.1:{{ webapp_listen_port }}/healthz"
status_code: 200
register: health
until: health.status == 200
retries: 12
delay: 5
changed_when: false
Sixty seconds of patience for a service that was just restarted. The condition is a state the system is moving towards, the action is read-only, and repeating it costs nothing.
The three properties that make this one honest:
- The action is idempotent, ideally read-only. A
GETon a health endpoint can run a hundred times. - The condition can become true without further intervention. The service is starting. Nobody has to do anything.
- The total wait is bounded and stated.
12 × 5= sixty seconds, in the file, where a reviewer can see whether that is the right number for this service.
The dishonest retries
Retrying a deterministic failure
The credential is wrong. The package name is misspelled. The path does not
exist. None of these become true by waiting, and a retry loop around them
does exactly one thing: it delays the alert by retries × delay.
The symptom is a play that used to fail in ten seconds and now fails in four minutes, with the operator watching a spinner. Worse, on a large fleet the delay is often long enough that somebody cancels the run, so the failure is never even reported — it is inferred from a Ctrl-C.
Test: if you cannot name the thing that will change between attempts, the retry is not doing anything.
Retrying a non-idempotent action
This is the one that causes damage rather than delay.
# Do not do this.
- name: create the reporting user
ansible.builtin.command: /usr/local/bin/create-user reporting
register: r
until: r.rc == 0
retries: 5
delay: 10
If the command creates the user and then fails — on a slow post-creation step, on a notification to a downstream service, on a timeout after the work was done — the retry runs it again. Five times. You now have five reporting users, or one user and four confusing failures, or a downstream system that received five conflicting notifications.
The retry did not absorb a fault. It amplified one.
Retrying to paper over a race
A retry loop added because “sometimes the config file is not there yet” is a
missing ordering constraint wearing a disguise. The fix is to make the
producing task run first, or to wait for the specific thing with
ansible.builtin.wait_for, which states what it is waiting for instead of
retrying something that happens to fail while it waits.
meta: clear_host_errors
A host that fails is removed from the play and does not appear in subsequent
plays of the same run. meta: clear_host_errors clears that state — and it
is narrower than people expect.
$ ansible-doc ansible.builtin.meta | grep -A4 clear_host_errors'clear_host_errors' (added in Ansible 2.1) clears the
failed state (if any) from hosts specified in the play's
list of hosts. This will make them available for
targeting in subsequent plays, but not continueIt does not resume the current play for the failed host. Verified: a
two-host play where one fails, a meta: clear_host_errors immediately after,
and a task following that:
$ ansible-playbook -i two.ini clearerr4.ymlTASK [node-a fails] ************************************************************
fatal: [node-a]: FAILED! => {"changed": false, "msg": "boom"}
skipping: [node-b]
TASK [reactivate failed hosts] *************************************************
TASK [both hosts should reach here] ********************************************
ok: [node-b] => {
"msg": "node-b is in play"
}
PLAY RECAP *********************************************************************
node-a : ok=0 changed=0 unreachable=0 failed=1
node-b : ok=1 changed=0 unreachable=0 failed=0What it does do is restore the host for the next play:
$ ansible-playbook -i two.ini clearerr5.yml; ansible-playbook -i two.ini clearerr6.yml=== WITH clear_host_errors ===
ok: [node-a] => { "msg": "node-a in play two" }
ok: [node-b] => { "msg": "node-b in play two" }
node-a : ok=1 changed=0 unreachable=0 failed=1
node-b : ok=1 changed=0 unreachable=0 failed=0
=== WITHOUT ===
ok: [node-b] => { "msg": "node-b in play two" }
node-a : ok=0 changed=0 unreachable=0 failed=1
node-b : ok=1 changed=0 unreachable=0 failed=0node-a appears in play two only when the meta task ran. And note the recap
in both cases: failed=1 for node-a. Clearing the error state does not
erase the failure from the statistics, which is correct — the run did contain
a failure and the recap says so.
Where it genuinely earns its place: a first play that probes an estate and tolerates failures, followed by a second play that should still consider every host. Used to make a failing host “come back”, it is a way of continuing past a failure without deciding to — which is the pattern lesson 7 spent its length arguing against.
Knowledge check
Knowledge check · 4 questions
Q1. A task carries until, retries: 3 and delay: 5, and the condition is never satisfied. How many times does the module execute, and roughly how long does the task take?
Q2. meta: clear_host_errors placed immediately after a failing task brings that host back into the current play, so subsequent tasks run for it again.
Q3. Which properties make a retry loop honest rather than harmful? Select all that apply.
Q4. A command creates a user, waits for a downstream confirmation, times out, and exits non-zero. Someone wraps it in until r.rc == 0 with retries: 5. What is the result?
Passing score: 75%. Answers are checked in this browser.