Ansible Asynchronous Tasks: Polling, Job IDs, and Safe Cleanup
Design async Ansible jobs with bounded runtimes, per-host job IDs, reliable polling, result checks, cleanup, and ansible-core 2.19-safe conditionals.
Ansible’s async keyword changes how long a task may run and when the playbook waits for it. It does not automatically make a task safe to overlap with later work. The important lifecycle is: choose the maximum runtime, decide whether Ansible should poll, preserve the returned job ID, collect the final result, check the application’s exit status, and remove the async status record only when it is safe to do so.
This distinction matters because poll: 0 is a launch-and-continue mode, not proof that the remote job completed. If a playbook ends without checking the job, Ansible has not confirmed its result. For long operations that affect packages, services, databases, or shared storage, the lifecycle and recovery behavior deserve the same attention as the task itself.
Choose between blocking and detached polling
Without async, Ansible runs a task synchronously. With async: N and a positive poll value, Ansible starts the task asynchronously but continues polling it; the next task waits until the remote operation completes, fails, or reaches its async time limit. The positive poll interval controls how often status is checked. The default poll interval comes from Ansible configuration, so set it explicitly when the operational cadence matters.
With poll: 0, Ansible starts the task and moves on immediately. It does not check back automatically before the play ends. The remote operation continues until it completes or exceeds its async limit, and the playbook must use ansible.builtin.async_status later if it needs a result or synchronization point.
The async value is a maximum runtime in seconds, not an estimate of how long the task should usually take. Set it above the expected duration with enough margin for realistic load, but not so high that a stuck task consumes resources indefinitely. If the async limit is too short, the task can time out before the later status check has a result to collect.
Use positive polling when later operations must wait for the result. Use poll: 0 only when continuing immediately is deliberate and concurrent execution is safe. The Ansible guide specifically cautions against using zero polling with operations that require exclusive locks, such as package-manager transactions, if later tasks in the same play will touch those resources.
Keep job IDs with their host and task
An async launch returns an ansible_job_id. async_status needs that ID to read the job’s status on the target. Register the launch result and pass that ID to the status task on the same host. In a multi-host play, treat the registered value as host-specific; do not copy one machine’s job ID into a shared variable and use it against other targets.
The status record is stored in the target’s async job cache, which defaults to ~/.ansible_async/ for the relevant remote user. Ansible’s async directory can be configured, so operational documentation should record the actual execution identity and cache location instead of assuming every privilege-escalation arrangement uses the same path. A job ID is useful only while the playbook can reach the host and the corresponding record is still present.
Here is a bounded example. It starts a controlled reindex command, polls each selected host’s job, checks the command’s return code explicitly, and removes the record in an always section only if polling established that the job finished.
- hosts: app_servers
gather_facts: false
tasks:
- name: Launch the reindex operation
ansible.builtin.command:
argv:
- /usr/local/sbin/reindex
- --no-input
async: 2700
poll: 0
register: reindex_launch
- name: Collect and validate the completed result
block:
- name: Wait for this host's reindex job
ansible.builtin.async_status:
jid: "{{ reindex_launch.ansible_job_id }}"
register: reindex_result
until: reindex_result is finished
retries: 240
delay: 10
# Keep a completed nonzero result available for the explicit assert.
failed_when: false
- name: Require a successful command exit
ansible.builtin.assert:
that:
- (reindex_result.rc | default(1) | int) == 0
fail_msg: "Reindex failed; inspect the captured result before retrying."
always:
- name: Remove the finished job's status record
ansible.builtin.async_status:
jid: "{{ reindex_launch.ansible_job_id }}"
mode: cleanup
when:
- reindex_result is defined
- reindex_result is finished
The retries and delay values are a polling budget: here they allow roughly 40 minutes of checks, while the job’s maximum runtime is 45 minutes. Align the two deliberately; otherwise the playbook can stop polling while the async process is still within its allowed runtime, or the task can time out before the polling window ends. Tune both values to the job’s service-level objective and deployment window.
failed_when: false prevents the poll task from hiding a completed nonzero command result before the assertion can inspect it. It does not turn a failed reindex into success: the assertion enforces rc == 0. An exhausted poll, unreachable target, missing job record, or timeout still needs an operator-defined failure path. If the job is incomplete, the always cleanup is skipped so the playbook does not intentionally erase the only status evidence it can still inspect.
Cleanup is not cancellation
For a task using poll: 0, Ansible does not automatically remove the async job cache file. ansible.builtin.async_status with mode: cleanup deletes that status record without waiting for the process to finish. Cleanup is therefore not a cancellation command. If you remove the record too early, the job can continue while later async_status calls can no longer use the record to report its lifecycle.
Capture the completed result before cleanup, retain the job ID and host in logs or orchestration state if incident recovery needs them, and keep useful but non-secret stdout, stderr, and return-code evidence. If a job must be stopped, use the operation’s documented cancellation or service-management interface; do not assume removing Ansible’s cache file stops remote execution.
When poll is positive, the async guide says Ansible automatically removes the temporary cache file when the task completes. Manual cleanup is mainly needed for zero-poll launches after their status has been collected. The cache directory is operational state, not a durable job queue or an audit ledger.
Treat concurrency and check mode explicitly
Zero-poll tasks can overlap with tasks that run immediately after the launch. That can be useful for independent workloads, but dangerous for package locks, migrations, maintenance scripts, and commands that expect exclusive access. If the action is not safe to overlap, wait with positive polling or add an explicit synchronization step before touching the same resource again. Bound the number of concurrent jobs through host selection and play batching; launching a large fleet at once is a capacity decision, not a free speedup.
The async guide also documents limitations around check mode. Do not assume --check executes an async operation harmlessly or validates its eventual behavior; skip it deliberately in check mode or exercise it against a disposable target. Modules and actions can have their own async support boundaries as well: for example, asynchronous mode does not transform a file-copy operation into a background transfer. Verify the module’s documented attributes and test the concrete action on the target platform.
Upgrade conditionals for ansible-core 2.19
The async status result exposes started and finished as integer properties in documented releases. Starting with ansible-core 2.19, the templating changes require boolean values in conditionals, so code that writes until: job_result.finished or when: job_result.finished can fail. The 2.19 porting guide recommends the corresponding test plugin instead:
until: job_result is finished
This asks Ansible’s finished test to return a boolean based on the status dictionary. It is not a change to the remote process or a guarantee that the job succeeded; it means only that the job reached a finished state. Inspect its rc, failed, or other module-specific result data afterward and decide whether the completed operation is acceptable.
Before a production rollout, validate the playbook syntax with the same ansible-core release used by automation, test both success and nonzero exit paths, test a job that remains pending through several polls, and verify what happens after the controller loses its connection. Confirm that the async timeout, retry window, job cache, and cleanup policy preserve enough information to recover without accidentally launching a duplicate migration or reindex.
Asynchronous tasks are easiest to operate when they have bounded runtimes, host-local job IDs, explicit result assertions, and a cleanup point after observation. poll: 0 removes waiting from the launch task; it does not remove responsibility for the process it started.
Related:
- Ansible Inventory in Production: Host Selection, Dynamic Sources, and Precedence
- Ansible Playbooks in Production: Idempotency, Check Mode, and Rolling Changes
Sources: