AnsibleXLIV · Failure Modes and Partial Fleet FailureFailure Modes and Partial Fleet Failure
When the connection dies mid-task
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
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.
| Stage | Happened? | If the link drops here |
|---|---|---|
| Connection opened | Known | Nothing ran. Host untouched |
| Temp directory created | Known | An empty directory in ~/.ansible/tmp |
| Module payload written to the remote | Known — the write returned | The file is there; nothing executed it |
| Module executed | Unknown | Anything between nothing and everything |
| Module wrote its JSON result to stdout | Unknown | The module may have finished perfectly |
| Result returned to the controller | Did not happen | This is why you are reading this |
| Temp directory removed | Did not happen | Evidence, 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:
- 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.
- 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.
- 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”.
$ ls -la ~/.ansible/tmp/ 2>/dev/null | tail -5 ; sudo ls -la /root/.ansible/tmp/ 2>/dev/null | tail -5drwx------ 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.pyIllustrative 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.
$ sudo dpkg --audit ; sudo dnf history list --reverse | tail -3The 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 EEIllustrative 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.
- 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: 30What 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
Q1. The SSH connection to a host drops while an apt task is executing. What does Ansible know about that host?
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.
Q3. Which artefact on the managed node best identifies which task was in flight when the connection died?
Q4. Which statements about async with poll 0 are accurate? Select all that apply.
Passing score: 75%. Answers are checked in this browser.