Async and Poll¶
async says how long a task may run in the background. poll: N checks it every N seconds; poll: 0 is fire-and-forget, to be checked later with async_status.
What You'll Learn¶
- What
asyncandpollcontrol, and the three combinations you'll use - How to launch a job, continue the play, and check on it later with
async_status - How to run slow loop items concurrently, and how to collect their results
- How async avoids SSH timeouts on long-running tasks
Why This Exists¶
A task that takes ten minutes (a large file download, a slow database migration) blocks the entire play for that host by default — async/poll lets Ansible kick it off and check back later, or fire-and-forget entirely.
Mental Model¶
async: Nsays "run this in the background on the managed node, and kill it if it runs longer than N seconds."poll: Msays "check back every M seconds and wait for it" — or, withpoll: 0, "don't wait at all; give me a job ID."
| Settings | Behavior |
|---|---|
no async |
Normal task: holds the connection until done |
async: 1800, poll: 15 |
Runs in the background but the play still waits, checking every 15 s. Avoids SSH/connection timeouts on long tasks |
async: 1800, poll: 0 |
Fire and forget: the play moves on immediately and you check later with async_status |
Waiting Without Holding a Connection¶
- name: Rebuild the search index (takes ~20 minutes)
ansible.builtin.command: /opt/search/bin/reindex --full
async: 3600
poll: 30
Without async, a 20-minute command holds an SSH session open the whole time and can be killed by an idle timeout on a firewall or bastion. With it, each check is a short, fresh connection.
Fire and Forget, Then Check Later¶
- name: Start the nightly backup in the background
ansible.builtin.command: /usr/local/bin/backup-db --target s3://backups/checkout
async: 7200 # hard ceiling: 2 hours
poll: 0
register: backup_job
- name: Meanwhile, rotate logs
ansible.builtin.command: logrotate -f /etc/logrotate.d/checkout
- name: Wait for the backup to finish
ansible.builtin.async_status:
jid: "{{ backup_job.ansible_job_id }}"
register: backup_result
until: backup_result.finished
retries: 240
delay: 30 # 240 × 30 s = up to 2 hours
- name: Clean up the job's status file
ansible.builtin.async_status:
jid: "{{ backup_job.ansible_job_id }}"
mode: cleanup
backup_result has the finished task's real result (rc, stdout, stderr), so you can use failed_when on it like any other result. Make retries × delay at least as long as async, or you'll stop waiting before the job's own ceiling.
A job ID belongs to one host
async_status must run on the same host that started the job. That happens naturally when both tasks are in the same play.
Running Loop Items Concurrently¶
A normal loop runs items one after another on each host. With poll: 0, each item is launched and the loop moves straight on, so the items do run at the same time:
- name: Download large model files in parallel
ansible.builtin.get_url:
url: "https://artifacts.example.com/models/{{ item }}"
dest: "/srv/models/{{ item }}"
loop:
- embeddings-v3.bin
- reranker-v2.bin
- classifier-v7.bin
async: 1800
poll: 0
register: downloads
- name: Wait for every download
ansible.builtin.async_status:
jid: "{{ item.ansible_job_id }}"
loop: "{{ downloads.results }}"
loop_control:
label: "{{ item.item }}"
register: download_status
until: download_status.finished
retries: 60
delay: 30
With poll greater than zero, the loop is still sequential: each item is backgrounded but waited on before the next starts. And none of this replaces host parallelism, which comes from forks — see Forks, Serial, Strategy, and Throttle.
Limiting Concurrency¶
Launching 50 heavy jobs at once can overwhelm a host or the service they call. Batch them with throttle at the task level, or split the list:
- name: Start at most 4 exports at a time per batch
ansible.builtin.include_tasks: export_batch.yml
loop: "{{ tenants | batch(4) | list }}"
loop_control:
loop_var: tenant_batch
Each included file launches its batch with poll: 0 and waits with async_status before the next batch begins.
When Not to Use Async¶
- Tasks later steps depend on. If the next task needs the result, a normal task (or
poll > 0) is clearer. - Check mode. Background jobs and check mode don't mix well; guard with
when: not ansible_check_mode. - Reboots. Use
ansible.builtin.reboot, which handles disconnect and reconnect for you.
Common Mistakes¶
- Assuming
asyncwithpoll > 0makes alooprun in parallel — it doesn't; onlypoll: 0plusasync_statusdoes. - Setting
asynctoo low, so the job is killed mid-run once the ceiling is hit. - Setting
retries × delayshorter than the job's real runtime, so the play fails while the job is still healthy. - Never checking a
poll: 0job, so failures are silent. - Forgetting
mode: cleanupon very frequent jobs, leaving status files to accumulate in~/.ansible_async.
Interview Questions¶
- What's the difference between
poll: 0and a normal blocking task? - How would you check on a fire-and-forget async task later in the same playbook?
- Why would you use
asyncwithpoll: 30rather than no async at all? - How do you download ten files concurrently on each host and still fail the play if one download fails?
Next¶
Continue to Advanced Execution.