Retry Loop Until
Before now, checking a task’s outcome meant one attempt, register, and is failed. until builds genuine retry logic on top of that same foundation — keep trying, on a delay, until a condition passes or the attempts run out.
until, retries, And delay
- name: Wait for a value
ansible.builtin.command: cat /tmp/ready_flag
register: result
until: result.rc == 0
retries: 5
delay: 3until: is the condition to check after each attempt. retries: is the maximum number of attempts. delay: is the number of seconds to wait between them.
How It Actually Works
Ansible runs the task, checks until: against the result it just registered. If true, it stops immediately — success. If false, it waits delay seconds and tries again, up to retries total attempts. If the condition never becomes true within that budget, the task is reported failed, exactly like any other task failure.
A Realistic Example: Waiting For A Service To Become Ready
tasks:
- name: "Wait for the app to report healthy"
ansible.builtin.command:
cmd: curl -s -o /dev/null -w "%{http_code}" http://localhost:8080
register: health_check
until: health_check.stdout == "200"
retries: 10
delay: 5
delegate_to: localhostA service that takes a few seconds to start up after being launched is exactly the case this pattern is built for — rather than guessing a fixed sleep duration and hoping it’s long enough, this keeps checking until the service is actually ready, or genuinely gives up after a defined budget.
delegate_to: localhost was delibate on my side because I have server running on my host localhost:8080.
Accessing The Final Result
Once the retry loop finishes — successfully or not — the registered variable holds the result of the last attempt, plus one extra field: attempts, showing how many tries it actually took.
- name: "Check until_output"
ansible.builtin.debug:
var: health_check
- name: "Report how many attempts it took"
ansible.builtin.debug:
msg: "Took {{ health_check.attempts }} attempt(s)"A Gotcha: until’s Registered Result Is Not A .results List
If you see the full output:
TASK [Wait for the app to report healthy] ******************************************************************************************************
FAILED - RETRYING: [fedora -> localhost]: Wait for the app to report healthy (9 retries left).
FAILED - RETRYING: [fedora -> localhost]: Wait for the app to report healthy (8 retries left).
changed: [fedora -> localhost]
TASK [Check until_output] ****************************************************************************
ok: [fedora] => {
"health_check": {
"attempts": 3,
"changed": true,
"cmd": [
"curl",
"-s",
"-o",
"/dev/null",
"-w",
"%{http_code}",
"http://localhost:8080"
],
"delta": "0:00:05.785893",
"end": "2026-09-01 15:32:02.674461",
"failed": false,
"msg": "",
"rc": 0,
"start": "2026-09-01 15:31:56.888568",
"stderr": "",
"stderr_lines": [],
"stdout": "200",
"stdout_lines": [
"200"
]
}
}Worth being explicit about, given how recently the loop-registration chapter covered this: until is not loop:, even though both involve running a task more than once. A looped, registered task gives you .results — a list, one entry per iteration, as covered in that chapter. until gives you a plain, single result, shaped exactly like an ordinary non-looped registered task, representing only the final attempt. There’s no list of every attempt’s individual outcome to inspect — just the last one, plus the attempts count. Don’t reach for .results here expecting the same structure; it isn’t there.
What Happens When All Retries Are Exhausted
If until: never becomes true within the retry budget, the task fails — normal failure handling applies from here, exactly as covered in the tests and conditionals section: the play halts for that host unless ignore_errors: true is set, and any later task checking is failed against this result would see it as failed, consistent with everything covered there.
Best Practices
- Always pair
until:withregister:, and setretries:/delay:to values that genuinely match how long the real operation might reasonably take — not an arbitrary guess. - Don’t confuse
until’s registered result with a looped task’s.resultslist — they look superficially similar (both involveregisterand running something more than once) but produce structurally different results. - Check
.attemptswhen it’s useful to know or log how many tries something actually needed — a task that always succeeds on the first attempt might not need retries configured as generously as one that regularly takes several.