Skip to main content
RunBook Academy

AnsibleXLIV · Failure Modes and Partial Fleet FailureFailure Modes and Partial Fleet Failure

When the connection dies mid-task

Expert⏱ ~26 minansible-playbookssh

What you'll learn

  • State exactly what Ansible knows and does not know after a mid-task disconnect
  • Explain why idempotency is a safety property rather than a style preference
  • Identify which task types are unsafe to re-run after an unknown-outcome interruption
  • Evaluate async with poll 0 and async_status as a partial mitigation

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.

Every other outcome in this part is a statement about the host. This one is a statement about your knowledge of the host, and it is the only outcome where the honest answer is that you do not have one.

The connection dropped after the module was delivered and before the result came back. Ansible reports the host UNREACHABLE. That word is doing something quite different here from the case in the last lesson.

What Ansible knows, and what it does not

A task runs in stages, and the disconnect can happen at any of them.

StageHappened?If the link drops here
Connection openedKnownNothing ran. Host untouched
Temp directory createdKnownAn empty directory in ~/.ansible/tmp
Module payload written to the remoteKnown — the write returnedThe file is there; nothing executed it
Module executedUnknownAnything between nothing and everything
Module wrote its JSON result to stdoutUnknownThe module may have finished perfectly
Result returned to the controllerDid not happenThis is why you are reading this
Temp directory removedDid not happenEvidence, and litter

The line in bold is the problem. From the controller’s point of view, “the module never ran” and “the module ran, completed, changed the host and the result was lost in transit” are indistinguishable. Both produce the same connection error, the same unreachable=1, and the same absence of a result.

This is why idempotency is a safety property

Most material on Ansible presents idempotency as good practice — cleaner reruns, honest changed reporting, better diffs. Those are real benefits and they are not the reason it matters.

The reason it matters is this lesson. The only way to recover from a task whose outcome is unknown is to be able to run it again safely, and that is the definition of idempotency.

Work through it. You have a host that dropped during task 17. You want it converged. Your options are:

  1. Inspect it manually and repair by hand. Correct, expensive, and does not scale past a handful of hosts. Also produces a snowflake unless you feed the repair back into the automation.
  2. Run the play again against that host. Free, instant, scales to fifty hosts — if and only if every task in the play is safe to execute against a host that may have already executed it.
  3. Rebuild the host. Correct in an immutable estate, disproportionate in most others.

Option 2 is the one that makes partial failures survivable at fleet scale, and it is available only if you wrote the play a particular way. Idempotency is not a style choice you make for elegance; it is the property that determines whether option 2 exists at 03:00.

The tasks that fail this test are exactly the tasks that make a rerun dangerous, which is the subject of the rerun-safety lesson later in this part. Here the point is narrower and sharper: those tasks are also the ones that make a mid-task disconnect unrecoverable without manual inspection. Non-idempotent tasks do not merely make reruns untidy. They remove your only cheap recovery path.

What the host looks like afterwards

There are physical traces, and they are worth knowing because they let you distinguish “never started” from “was running”.

Read-only / Safeevidence left on a host that was interrupted mid-task
$ ls -la ~/.ansible/tmp/ 2>/dev/null | tail -5 ; sudo ls -la /root/.ansible/tmp/ 2>/dev/null | tail -5
drwx------  2 svc-ansible svc-ansible 4096 Aug 11 02:31 ansible-tmp-1786489621.15-370915-2222077828
-rw-------  1 svc-ansible svc-ansible 82344 Aug 11 02:31 AnsiballZ_apt.py

Illustrative output

A leftover ansible-tmp-* directory with an AnsiballZ_<module>.py inside it names the module that was in flight and timestamps the moment the run stopped. That is a genuinely useful forensic artefact: it tells you which task was interrupted, which the controller-side log cannot, because the controller never learned that the module started.

Read-only / Safechecking a package manager for an interrupted transaction
$ sudo dpkg --audit ; sudo dnf history list --reverse | tail -3
The following packages are only half configured, probably due to problems
configuring them the first time.  The configuration should be retried using
dpkg --configure <package> or the configure menu in dselect:
linux-image-6.8.0-51-generic

ID     | Command line             | Date and time    | Action(s) | Altered
------ | ------------------------ | ---------------- | --------- | -------
  47 | upgrade --assumeyes      | 2026-08-11 02:29 | E, I, U   |     114 EE

Illustrative output

An interrupted package transaction is the worst realistic case, because the package manager holds locks and refuses to proceed until a human resolves the state — which means the next automated run against that host fails too, on a different task, for a reason that has nothing to do with the play.

async with poll: 0, and its own failure modes

The documented mitigation for long-running tasks is to detach them. async: <seconds> with poll: 0 starts the module on the managed node in the background, returns a job id immediately, and lets a later async_status task collect the result.

Service impact possibledetaching a long operation so a dropped link does not lose it
- name: Start the upgrade without holding the connection open
ansible.builtin.apt:
  upgrade: dist
  lock_timeout: 300
async: 3600
poll: 0
register: upgrade_job

- name: Collect the result, retrying if the link is flaky
ansible.builtin.async_status:
  jid: '{{ upgrade_job.ansible_job_id }}'
register: upgrade_result
until: upgrade_result.finished
retries: 120
delay: 30

What this genuinely buys you: the operation is no longer coupled to a single continuous SSH session. If the link drops during the hour the upgrade takes, the upgrade carries on, and the async_status task reconnects and finds out how it went. The unknown-outcome window shrinks from “the whole task” to “the moment of launch”.

Use async for genuinely long operations where a dropped connection is likely: large upgrades, long-running data operations, anything measured in tens of minutes. Do not use it as a general fix for flakiness. A fleet where connections drop often enough to need this everywhere has a network problem that async is hiding.

Knowledge check

Knowledge check · 4 questions

  1. Q1. The SSH connection to a host drops while an apt task is executing. What does Ansible know about that host?

  2. Q2. Idempotency matters here because it is the property that determines whether re-running the play is an available recovery option after an unknown-outcome interruption.

  3. Q3. Which artefact on the managed node best identifies which task was in flight when the connection died?

  4. Q4. Which statements about async with poll 0 are accurate? Select all that apply.

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