Skip to main content
RunBook Academy

AnsibleXXXI · Serial Execution and Failure ToleranceFailure Tolerance

any_errors_fatal and what fatal means

Advanced⏱ ~23 minansible-playbook

What you'll learn

  • Describe exactly how far execution proceeds after any_errors_fatal triggers
  • Explain why a later play in the same playbook does not run
  • Apply any_errors_fatal at block level to scope it to a critical section
  • State what any_errors_fatal does not do about 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

Not yet marked complete on this device.

max_fail_percentage expresses a tolerance: some failures are acceptable, past a threshold they are not. any_errors_fatal expresses the opposite position — one failure anywhere means the change is wrong, so stop touching things.

Upstream describes it precisely, and the precision is the whole lesson:

If you set any_errors_fatal and a task returns an error, Ansible finishes the fatal task on all hosts in the current batch and then stops executing the play on all hosts.

Two clauses. The first is a courtesy; the second is much bigger than it sounds.

Finishing the fatal task on all hosts in the batch

The failing task is not abandoned partway. Every host in the batch runs it, and the results all appear, before anything stops.

Read-only / Safesix hosts, serial: 2, h03 fails its assertion — executed on ansible-core 2.21.3
$ ansible-playbook -i inv6.ini aef.yml
PLAY [Rollout] *****************************************************************

TASK [Deploy] ******************************************************************
ok: [h01] => { "msg": "DEPLOYED h01" }
ok: [h02] => { "msg": "DEPLOYED h02" }

TASK [Health check] ************************************************************
ok: [h01] => { "msg": "HEALTHY h01" }
ok: [h02] => { "msg": "HEALTHY h02" }

TASK [Return to service] *******************************************************
ok: [h01] => { "msg": "INSERVICE h01" }
ok: [h02] => { "msg": "INSERVICE h02" }

PLAY [Rollout] *****************************************************************

TASK [Deploy] ******************************************************************
ok: [h03] => { "msg": "DEPLOYED h03" }
ok: [h04] => { "msg": "DEPLOYED h04" }

TASK [Health check] ************************************************************
fatal: [h03]: FAILED! => {"assertion": "inventory_hostname not in ['h03']",
                        "changed": false, "evaluated_to": false,
                        "msg": "health check failed"}
ok: [h04] => { "changed": false, "msg": "HEALTHY h04" }

PLAY RECAP *********************************************************************
h01                        : ok=3    changed=0    unreachable=0    failed=0
h02                        : ok=3    changed=0    unreachable=0    failed=0
h03                        : ok=1    changed=0    unreachable=0    failed=1
h04                        : ok=2    changed=0    unreachable=0    failed=0

h04 ran the health check and passed it, after h03 was already fatal. That is the first clause working: the task completes across the batch.

h04 then did not run “Return to service”. That is the second clause, and it produces the same stranded-host problem the previous lesson described for max_fail_percentage: a healthy host, drained, never returned.

Stopping the play on all hosts

Read the recap again. h05 and h06 are not in it.

They were in batch three. Batch three never ran. any_errors_fatal did not merely end the batch that contained the failure — it ended the play, and every host that had not yet been reached was never reached.

That is the intended behaviour and it is the reason to choose this keyword. But it extends further than most people expect.

Scoping it to a block

any_errors_fatal is settable at play level and at block level. Block level is usually the better instrument, because most playbooks contain one genuinely critical section and a lot of ordinary work around it.

Service impact possiblefatal only where it needs to be
- name: Apply the release
hosts: appservers
become: true
serial: 2
tasks:
  - name: Stage the release artefact
    ansible.builtin.copy:
      src: 'app-{{ release_version }}.tar.gz'
      dest: /var/tmp/app-release.tar.gz
      mode: '0640'

  - name: Schema-affecting migration
    any_errors_fatal: true
    block:
      - name: Verify the migration is reversible before applying it
        ansible.builtin.command: /usr/local/bin/app-migrate --dry-run
        changed_when: false

      - name: Apply the migration
        ansible.builtin.command: /usr/local/bin/app-migrate --apply
        register: migration
        changed_when: "'applied' in migration.stdout"

  - name: Record the deployed version
    ansible.builtin.copy:
      content: '{{ release_version }}'
      dest: /etc/app/deployed-version
      mode: '0644'

Verified on 2.21.3 with the block form: serial: 2 over six hosts, a failure on h03 inside the block. h01 and h02 completed the play, h04 did not run the task after the block, and h05 and h06 never ran at all — identical to the play-level behaviour, scoped to the block.

It does not make unreachable hosts fatal

This is the finding that surprised me most while preparing this lesson, and it is worth stating as an exception because the keyword name promises otherwise.

The upstream sentence says “if a task returns an error”. A host that cannot be connected to does not return an error from a task — it never ran one. Ansible marks it UNREACHABLE and, per the documentation, “removes it from the list of active hosts for the run”. That is a different path, and any_errors_fatal does not intercept it.

Read-only / Safeany_errors_fatal with an unreachable host — executed on 2.21.3
$ ansible-playbook -i inv-unreach.ini rollout.yml
PLAY [Rollout] *****************************************************************
TASK [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}

PLAY [Rollout] *****************************************************************
TASK [Touch the host] **********************************************************
ok: [h04]
ok: [h05]
ok: [h06]
TASK [Return to service] *******************************************************
ok: [h04] => { "msg": "INSERVICE h04" }
ok: [h05] => { "msg": "INSERVICE h05" }
ok: [h06] => { "msg": "INSERVICE h06" }

PLAY [Second play] *************************************************************
TASK [Monitoring] **************************************************************
ok: [h04] => { "msg": "MONITORING h04" }
ok: [h05] => { "msg": "MONITORING h05" }
ok: [h06] => { "msg": "MONITORING h06" }

PLAY RECAP *********************************************************************
h02 : ok=1 changed=0 unreachable=0 failed=0
h03 : ok=1 changed=0 unreachable=0 failed=0
h04 : ok=3 changed=0 unreachable=0 failed=0
h05 : ok=3 changed=0 unreachable=0 failed=0
h06 : ok=3 changed=0 unreachable=0 failed=0
u01 : ok=0 changed=0 unreachable=1 failed=0
exit=4

Compare the two runs directly. Same keyword, same serial, same playbook shape, failure in batch one:

Host failed a taskHost was unreachable
Rest of the failing batchstops after the fatal taskstops after the fatal task
Later batchesnever runrun to completion
Later play in the playbooknever runsruns, for the later-batch hosts
Exit code24

The batch containing the unreachable host was cut — h02 and h03 never returned to service, and they are absent from the second play, so they were dropped from the run. But the rollout itself continued.

any_errors_fatal is not a defence against a fleet that is disappearing. Neither is max_fail_percentage. If the risk you are managing is connectivity rather than correctness, the mechanism is a reachability precondition in an unbatched play before the rollout starts — a subject the next lesson takes up in full.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A play has any_errors_fatal: true and serial: 2 over six hosts. Host h03 fails a task in batch two. What does h04, its batch-mate, do?

  2. Q2. A playbook has a rolling play with any_errors_fatal: true, followed by a second play that re-enables monitoring. A host fails in batch two of five. What is true afterwards? Select all that apply.

  3. Q3. With any_errors_fatal: true, a host that is unreachable rather than failed will stop the whole run in the same way a failed host does.

  4. Q4. Which change is the best candidate for any_errors_fatal rather than max_fail_percentage?

Passing score: 75%. Answers are checked in this browser.