Ansible async and poll: Run Long Tasks Without Timeout Complete Guide
By Luca Berton · Published 2024-01-01 · Category: troubleshooting
Complete guide to Ansible async and poll for long-running tasks. Learn fire-and-forget patterns, polling strategies, parallel execution, async with loops.
When an Ansible task takes longer than the SSH timeout (default 30 seconds), it fails. The async and poll keywords solve this — run tasks in the background, check their status periodically or later, and handle timeouts gracefully. Here's every pattern you need.
How async and poll Work
- name: Long task
ansible.builtin.command: /opt/run-migration.sh
async: 3600 # Maximum runtime in seconds
poll: 10 # Check every N seconds (0 = fire-and-forget)
• async: N — Maximum seconds the task can run before Ansible kills it
• poll: N — How often (seconds) Ansible checks if the task finished
• poll: 0 — Don't wait. Fire-and-forget. Check later with async_status
See also: Ansible Performance Optimization: Speed Up Playbooks for Large-Scale Environments
Pattern 1: Poll Until Complete
The simplest pattern — Ansible waits, checking periodically:
---
- name: Run long task with polling
hosts: servers
tasks:
- name: Full system upgrade
ansible.builtin.apt:
upgrade: dist
update_cache: true
async: 1800 # Allow up to 30 minutes
poll: 30 # Check every 30 seconds
Ansible prints status updates every 30 seconds until the task completes or hits the 1800-second limit.
Pattern 2: Fire-and-Forget
Start the task and move on immediately:
- name: Start backup (don't wait)
ansible.builtin.command: /opt/backup/full-backup.sh
async: 7200 # Max 2 hours
poll: 0 # Don't wait
register: backup_job
- name: Continue with other tasks
ansible.builtin.debug:
msg: "Backup running in background, doing other work..."
- name: More work while backup runs
ansible.builtin.apt:
name: nginx
state: present
See also: Ansible Async: Run Long-Running Tasks in Background (Complete Guide)
Pattern 3: Fire-and-Check-Later
The most powerful pattern — start tasks in parallel, then check all at once:
---
- name: Parallel operations
hosts: servers
tasks:
# Start multiple long tasks simultaneously
- name: Start database migration
ansible.builtin.command: /opt/migrate-db.sh
async: 3600
poll: 0
register: db_migration
- name: Start cache warmup
ansible.builtin.command: /opt/warm-cache.sh
async: 1800
poll: 0
register: cache_warmup
- name: Start index rebuild
ansible.builtin.command: /opt/rebuild-indexes.sh
async: 3600
poll: 0
register: index_rebuild
# Do other work while all three run
- name: Configure nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
# Now check all jobs
- name: Wait for database migration
ansible.builtin.async_status:
jid: "{{ db_migration.ansible_job_id }}"
register: db_result
until: db_result.finished
retries: 120
delay: 30
- name: Wait for cache warmup
ansible.builtin.async_status:
jid: "{{ cache_warmup.ansible_job_id }}"
register: cache_result
until: cache_result.finished
retries: 60
delay: 30
- name: Wait for index rebuild
ansible.builtin.async_status:
jid: "{{ index_rebuild.ansible_job_id }}"
register: index_result
until: index_result.finished
retries: 120
delay: 30
Pattern 4: Async with Loops
Run the same long task across multiple items in parallel:
- name: Update all databases
ansible.builtin.command: "/opt/update-db.sh {{ item }}"
async: 3600
poll: 0
register: db_updates
loop:
- users_db
- orders_db
- analytics_db
- logs_db
- name: Wait for all database updates
ansible.builtin.async_status:
jid: "{{ item.ansible_job_id }}"
register: db_results
until: db_results.finished
retries: 120
delay: 15
loop: "{{ db_updates.results }}"
See also: Ansible Run Playbooks in Parallel: Async, Forks & Concurrent Execution
Pattern 5: Conditional Async Check
- name: Start long process
ansible.builtin.command: /opt/process-data.sh
async: 7200
poll: 0
register: data_job
- name: Do other work
ansible.builtin.include_tasks: other-tasks.yml
- name: Check if job finished (non-blocking)
ansible.builtin.async_status:
jid: "{{ data_job.ansible_job_id }}"
register: job_status
ignore_errors: true
- name: Handle job still running
ansible.builtin.debug:
msg: "Job still running — will check again later"
when: not job_status.finished
- name: Handle job complete
ansible.builtin.debug:
msg: "Job finished with rc={{ job_status.rc }}"
when: job_status.finished
Real-World Examples
Large Package Upgrade
- name: Upgrade all packages on fleet
hosts: all
serial: 5 # 5 hosts at a time
tasks:
- name: Full upgrade
ansible.builtin.apt:
upgrade: full
update_cache: true
autoremove: true
async: 3600
poll: 60
register: upgrade_result
- name: Check if reboot needed
ansible.builtin.stat:
path: /var/run/reboot-required
register: reboot_required
- name: Reboot if needed
ansible.builtin.reboot:
reboot_timeout: 300
when: reboot_required.stat.exists
Database Migration with Rollback
- name: Database migration
hosts: db_servers
tasks:
- name: Create backup first
ansible.builtin.command: pg_dump myapp > /tmp/pre-migration.sql
async: 1800
poll: 30
- name: Run migration
ansible.builtin.command: /opt/app/migrate.sh
async: 3600
poll: 0
register: migration_job
- name: Check migration status
ansible.builtin.async_status:
jid: "{{ migration_job.ansible_job_id }}"
register: migration_result
until: migration_result.finished
retries: 120
delay: 30
ignore_errors: true
- name: Rollback on failure
ansible.builtin.command: psql myapp < /tmp/pre-migration.sql
when: migration_result.failed | default(false)
- name: Fail if migration failed
ansible.builtin.fail:
msg: "Migration failed and was rolled back"
when: migration_result.failed | default(false)
Parallel File Transfers
- name: Deploy large artifacts in parallel
hosts: app_servers
tasks:
- name: Download artifacts (parallel)
ansible.builtin.get_url:
url: "{{ item.url }}"
dest: "{{ item.dest }}"
checksum: "{{ item.checksum }}"
async: 600
poll: 0
register: downloads
loop:
- url: https://artifacts.example.com/app-2.0.tar.gz
dest: /opt/releases/app-2.0.tar.gz
checksum: "sha256:abc123..."
- url: https://artifacts.example.com/assets-2.0.tar.gz
dest: /opt/releases/assets-2.0.tar.gz
checksum: "sha256:def456..."
- url: https://artifacts.example.com/config-2.0.tar.gz
dest: /opt/releases/config-2.0.tar.gz
checksum: "sha256:ghi789..."
- name: Wait for all downloads
ansible.builtin.async_status:
jid: "{{ item.ansible_job_id }}"
register: download_results
until: download_results.finished
retries: 40
delay: 15
loop: "{{ downloads.results }}"
Service Health Check After Deploy
- name: Deploy and verify
hosts: web_servers
tasks:
- name: Deploy new version
ansible.builtin.copy:
src: app-2.0/
dest: /opt/app/
notify: restart app
- name: Flush handlers
ansible.builtin.meta: flush_handlers
- name: Wait for service to be healthy
ansible.builtin.uri:
url: "http://localhost:8080/health"
status_code: 200
register: health
until: health.status == 200
retries: 30
delay: 10
async: 600
poll: 0
async_status Module Reference
- name: Check job status
ansible.builtin.async_status:
jid: "{{ job.ansible_job_id }}"
mode: status # 'status' (default) or 'cleanup'
register: result
# result fields:
# result.finished — 0 (running) or 1 (done)
# result.rc — return code (when finished)
# result.stdout — stdout output
# result.stderr — stderr output
# result.failed — true if task failed
# Clean up async job artifacts
- name: Remove job artifacts
ansible.builtin.async_status:
jid: "{{ job.ansible_job_id }}"
mode: cleanup
Timeouts and Error Handling
Task Exceeds async Limit
# If the task runs longer than async seconds, Ansible kills it
- name: Task with strict timeout
ansible.builtin.command: /opt/process.sh
async: 300 # Hard kill after 5 minutes
poll: 10
# To handle timeout gracefully:
- name: Task with timeout handling
ansible.builtin.command: /opt/process.sh
async: 300
poll: 0
register: job
- name: Check with timeout handling
ansible.builtin.async_status:
jid: "{{ job.ansible_job_id }}"
register: result
until: result.finished
retries: 20
delay: 15
ignore_errors: true
- name: Handle timeout
ansible.builtin.debug:
msg: "Task timed out — cleaning up"
when: result.finished == 0 or result.failed | default(false)
Common Mistakes
Mistake 1: async Without poll on Non-Command Modules
# ❌ Some modules don't support async well
- ansible.builtin.service:
name: nginx
state: restarted
async: 60
poll: 0
# Fire-and-forget on service restart can cause race conditions
# ✅ Better: use poll > 0 for state-changing modules
- ansible.builtin.service:
name: nginx
state: restarted
async: 60
poll: 5
Mistake 2: Forgetting to Check async_status
# ❌ Fire-and-forget with no check = silent failures
- ansible.builtin.command: /opt/migrate.sh
async: 3600
poll: 0
# If this fails, you'll never know
# ✅ Always check critical jobs
- ansible.builtin.command: /opt/migrate.sh
async: 3600
poll: 0
register: migrate_job
- name: Verify migration completed
ansible.builtin.async_status:
jid: "{{ migrate_job.ansible_job_id }}"
register: result
until: result.finished
retries: 120
delay: 30
failed_when: result.rc != 0
Mistake 3: async Value Too Small
# ❌ async is a TIMEOUT, not a delay
- ansible.builtin.apt:
upgrade: dist
async: 60 # Kills upgrade after 60 seconds!
poll: 10
# ✅ Set async much higher than expected runtime
- ansible.builtin.apt:
upgrade: dist
async: 3600 # Allow up to 1 hour
poll: 30
FAQ
What happens if the controller disconnects during an async task?
The task continues running on the remote host. When Ansible reconnects, you can check the job status using async_status with the job ID (if you stored it). The job artifacts are stored in ~/.ansible_async/ on the remote host.
Can I use async with become (sudo)?
Yes. Async tasks respect become settings. The background process runs as the become user.
Does async work with all modules?
Most command-execution modules work well with async. Some modules (like debug, set_fact, meta) don't benefit from async since they execute locally. Modules that require persistent connections may behave differently with poll: 0.
How do I set a global async timeout?
There's no global setting. Set async per task based on expected runtime. For a project-wide default, use a variable:
vars:
default_async: 3600
default_poll: 30
tasks:
- name: Long task
ansible.builtin.command: /opt/process.sh
async: "{{ default_async }}"
poll: "{{ default_poll }}"
What's the difference between async timeout and SSH timeout?
SSH timeout (timeout in ansible.cfg) controls the initial connection. async controls how long a background task can run. Without async, a task that outlasts the SSH timeout fails. With async, the task runs independently of the SSH session.
Conclusion
Use async + poll > 0 for long-running tasks that would timeout over SSH. Use poll: 0 + async_status for fire-and-forget parallel execution. Always check critical async jobs with async_status. Set async significantly higher than expected runtime — it's a timeout, not a target.
Related Articles
• Ansible Performance Tuning • Ansible run_once, delegate_to, serial • Ansible shell vs command vs raw • Ansible Error Handling GuideCategory: troubleshooting