AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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 TuningAnsible run_once, delegate_to, serialAnsible shell vs command vs rawAnsible Error Handling Guide

Category: troubleshooting

Browse all Ansible tutorials · AnsiblePilot Home