AnsibleXXXI · Serial Execution and Failure ToleranceFailure Tolerance
max_fail_percentage, precisely
What you'll learn
- State whether max_fail_percentage is evaluated per batch or per play, and prove it
- Apply the exceeded-not-equalled rule to choose a threshold that aborts when you intend
- Predict which hosts are left unfinished when the threshold is exceeded
- Explain why max_fail_percentage does not respond to unreachable hosts
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
By default, a play keeps going as long as some host is still succeeding. Upstream puts it plainly: “Ansible continues to execute tasks as long as there are hosts that have not yet failed.”
On a rolling deploy that default is dangerous. The change fails on host one, and the play does not care — it proceeds to host two, fails again, proceeds to host three. The mechanism that was supposed to protect you by staging the change happily stages a fleet-wide breakage in comfortable instalments.
max_fail_percentage is the threshold that stops it. It has two rules
that are not obvious from reading it, and both of them can be
demonstrated in a few seconds.
Rule one: it is evaluated per batch
Upstream states this: “The max_fail_percentage setting applies to each
batch when you use it with serial.”
Worth proving rather than accepting, because “per batch” and “per play” produce identical results in the common case of equal batches with evenly spread failures, and the difference only shows up when they are uneven — which is precisely the canary shape you will actually deploy.
The discriminating experiment uses unequal batches and a single failure:
- name: Rollout
hosts: batchlab
gather_facts: false
serial:
- 4
- 2
max_fail_percentage: 30
tasks:
- name: Deploy
ansible.builtin.debug:
msg: 'DEPLOYED {{ inventory_hostname }}'
- name: Health check
ansible.builtin.fail:
msg: health check failed
when: inventory_hostname in ['h05']
- name: Return to service
ansible.builtin.debug:
msg: 'INSERVICE {{ inventory_hostname }}'If the threshold is compared against the batch, one failure out of two is 50%, which exceeds 30, and the run aborts. If it is compared against the play, one failure out of six is 16.7%, which does not exceed 30, and h06 finishes normally.
$ ansible-playbook -i inv6.ini discriminator.ymlmax_fail_percentage: 30
deployed: h01 h02 h03 h04 h05 h06
FAILED: h05
returned: h01 h02 h03 h04 <-- h06 missing
exit=2
max_fail_percentage: 60 (control: 50% does not exceed 60)
deployed: h01 h02 h03 h04 h05 h06
FAILED: h05
returned: h01 h02 h03 h04 h06 <-- h06 present
exit=2Per batch, confirmed. The control run with a threshold of 60 is the same playbook with the same single failure, and h06 completes — which establishes that the abort in the first run was caused by the threshold and not by anything else about h05.
Without serial, the whole play is a single batch, and the
threshold applies to it. Verified: six hosts, no serial, one failure
(16.7%) and max_fail_percentage: 30 runs to completion; three failures
(50%) and the same threshold aborts.
Rule two: exceeded, not equalled
The comparison is strictly greater-than. Upstream is explicit, and gives the example in the same breath:
The percentage set must be exceeded, not equaled. For example, if serial were set to 4 and you wanted the task to abort the play when 2 of the systems failed, set the max_fail_percentage at 49 rather than 50.
Reproduced here exactly:
$ ansible-playbook -i inv6.ini boundary.ymlserial: 4, h01 and h02 fail -> 2/4 = 50.0%
max_fail_percentage: 49
deployed: h01 h02 h03 h04
FAILED: h01 h02
returned: (none)
exit=2 <-- aborted, later batch never ran
max_fail_percentage: 50
deployed: h01 h02 h03 h04 h05 h06
FAILED: h01 h02
returned: h03 h04 h05 h06
exit=2 <-- continued, whole fleet processed50 does not stop a 50% failure rate. This is the single most common
misconfiguration of this keyword, and it fails in the most expensive
direction: the person who wrote max_fail_percentage: 50 believed they
had asked for “stop if half the batch breaks”, and what they actually
asked for is “stop if more than half breaks”.
The boundary is equally sharp at other ratios:
$ ansible-playbook -i inv6.ini boundary33.ymlserial: 3, h01 fails -> 1/3 = 33.33%
max_fail_percentage: 33
TASK [Deploy] ok: h01 h02 h03
TASK [Health check] fatal: h01 skipping: h02 h03
NO MORE HOSTS LEFT
(Return to service never ran; h04 h05 h06 never appeared)
exit=2
max_fail_percentage: 34
TASK [Deploy] ok: h01 h02 h03
TASK [Health check] fatal: h01 skipping: h02 h03
TASK [Return to service] ok: h02 h03
PLAY [Rollout] (batch 2)
TASK [Deploy] ok: h04 h05 h06
TASK [Return to service] ok: h04 h05 h06
exit=2What an abort actually leaves behind
This is the part that is not in the documentation and that matters more operationally than either rule above.
When the threshold is exceeded, the play does not finish the current batch. It stops at the end of the failing task, and the healthy hosts in that batch do not run the remaining tasks.
Look again at the 49 run:
max_fail_percentage: 49
deployed: h01 h02 h03 h04
FAILED: h01 h02
returned: (none)
h03 and h04 passed their health check. They are fine. They were
deployed to, and they never reached “Return to service”. In a real
rolling play, “Return to service” is the task that puts the host back in
the load balancer — so those two hosts are drained, healthy, running the
new version, and receiving no traffic.
The recap will not tell you this. It reports h03 and h04 as
failed=0, because nothing failed on them:
$ ansible-playbook -i inv6.ini abort.ymlPLAY RECAP *********************************************************************
h01 : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
h02 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
h03 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0Three things to read out of that recap, none of which are stated in it:
h02andh03are unfinished, not successful.failed=0means no task failed on them. It does not mean the play completed for them. Compareok=1here againstok=2in the run that completed.h04,h05andh06are missing entirely. A host that was never reached gets no recap line at all. The absence of a line is the only evidence that later batches did not run.- The exit code is
2, the same as any other task failure. Nothing in the exit code distinguishes “one host failed and the rest completed” from “the rollout aborted with a batch half-finished”.
Unreachable hosts do not count
Stated here because it changes what this keyword is for, and covered in full in the unreachable-versus-failed lesson.
max_fail_percentage responds to failed hosts. It does not respond
to unreachable ones.
$ ansible-playbook -i inv-unreach.ini rollout.ymlTASK [Touch the host] **********************************************************
ok: [h02]
ok: [h03]
fatal: [u01]: UNREACHABLE! => {"changed": false, "msg": "Task failed: Failed to
connect to the host via ssh: ssh: connect to host 192.0.2.11 port 22: Connection
timed out", "unreachable": true}
TASK [Return to service] *******************************************************
ok: [h02]
ok: [h03]
... all remaining batches ran to completion ...
PLAY RECAP *********************************************************************
h02 : ok=2 changed=0 unreachable=0 failed=0
...
u01 : ok=0 changed=0 unreachable=1 failed=0
exit=4One host out of three in the batch was unreachable — 33.3%, against a threshold of zero — and the play ran the entire fleet.
The operational reading: max_fail_percentage is not a circuit
breaker for a fleet that is falling off the network. A rollout during
a partial network outage will proceed through every batch, touching
whatever it can reach, no matter how strict the threshold. If you need
to stop on connectivity loss, you need a different mechanism.
Knowledge check
Knowledge check · 4 questions
Q1. A play uses serial: 4 and max_fail_percentage: 50. Two of the four hosts in the first batch fail. What happens?
Q2. A play uses serial: [4, 2] and max_fail_percentage: 30 over six hosts. One host fails, and it is in the second batch. Batch-2 failure rate is 50%; play-wide rate is 16.7%. What does this run demonstrate?
Q3. A rolling play trips max_fail_percentage during batch two. Which statements about the state of the fleet are true? Select all that apply.
Q4. With max_fail_percentage: 0 and serial: 3, a batch in which one of the three hosts is unreachable will abort the run.
Passing score: 75%. Answers are checked in this browser.