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

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.

See also: Ansible Performance Optimization: Speed Up Playbooks for Large-Scale Environments

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

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

See also: Ansible Async: Run Long-Running Tasks in Background (Complete Guide)

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 }}"

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

See also: Ansible Run Playbooks in Parallel: Async, Forks & Concurrent Execution

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.

Category: troubleshooting

Browse all Ansible tutorials · AnsiblePilot Home