Fix Ansible Async Task Timeout: "Timeout waiting for async task" and async_status Not Returning Results

intermediate๐Ÿ”ง Ansible2026-05-07| Ansible 2.9+, any Linux/Unix remote host, long-running shell/command/yum/apt tasks

Error Message

FAILED! => {"msg": "Timeout (30s) waiting for async task to finish", "finished": 0, "started": 1}
#async#async_status#timeout#poll#long-running-tasks

The Error

You kicked off a long-running task with async and poll: 0, then used async_status to collect the result. Instead of a success, you get this:

FAILED! => {"msg": "Timeout (30s) waiting for async task to finish", "finished": 0, "started": 1}

"finished": 0 is the key detail โ€” the task was still running when async_status gave up. It started fine. It just needed more time than you gave it.

Why This Happens

With poll: 0, Ansible fires the task and immediately moves on. No waiting. Later, async_status polls for the result by looping with retries and delay. The timeout fires when either of two things goes wrong:

  • The retries ร— delay budget runs out before the task finishes.
  • The async value itself is too small โ€” Ansible kills the remote process after that many seconds.

Here's the default polling loop you see in most playbooks. Do the math: 10 retries ร— 3 seconds = 30 seconds total. That's fine for a quick script, but it won't survive a package install, database migration, or anything that actually takes time.

- async_status:
    jid: "{{ async_result.ansible_job_id }}"
  register: job_result
  until: job_result.finished
  retries: 10
  delay: 3   # 10 ร— 3 = 30 seconds total wait

Step-by-Step Fix

Step 1 โ€” Measure how long your task actually takes

SSH into the remote host and time it directly:

time apt-get install -y some-large-package
# or
time ./long_migration_script.sh

Take that number and add a 50โ€“100% buffer. A task that runs in 3 minutes on a fast machine might take 6 minutes on an overloaded server at 2am. Budget for the worst case.

Step 2 โ€” Set async high enough

async is the hard kill timer on the remote host. Once that many seconds pass, Ansible terminates the process โ€” no matter how close it was to finishing. Set it well above your worst-case runtime:

- name: Run long database migration
  command: /opt/app/migrate.sh
  async: 600      # allow up to 10 minutes
  poll: 0
  register: migration_job

Step 3 โ€” Fix the async_status polling loop

Bump retries and delay so their product matches (or exceeds) your async value:

- name: Wait for migration to complete
  async_status:
    jid: "{{ migration_job.ansible_job_id }}"
  register: job_result
  until: job_result.finished
  retries: 60     # check up to 60 times
  delay: 10       # every 10 seconds = 600 seconds total

The rule is simple: retries ร— delay must be โ‰ฅ async. If the polling loop gives up first, you get the timeout error even though the task is still running fine on the remote host.

Complete working example

---
- hosts: app_servers
  tasks:
    - name: Run package upgrade (long task)
      apt:
        upgrade: dist
      async: 900        # 15-minute hard cap on remote
      poll: 0
      register: apt_job

    - name: Wait for package upgrade
      async_status:
        jid: "{{ apt_job.ansible_job_id }}"
      register: apt_result
      until: apt_result.finished
      retries: 90       # 90 ร— 10s = 900s โ€” matches async value
      delay: 10

    - name: Show upgrade result
      debug:
        var: apt_result

Step 4 โ€” Skip all this if you don't need fire-and-forget

Fire-and-forget with poll: 0 is only useful when you need to kick off multiple long tasks in parallel, or do other work while something slow runs. For a single long task, just drop poll: 0 and let Ansible handle the polling itself:

- name: Run long task with built-in polling
  command: /opt/app/migrate.sh
  async: 600
  poll: 15      # Ansible checks every 15 seconds, no async_status needed

Fewer moving parts, same result.

Verify the Fix

Run with -v to watch the polling in real time:

ansible-playbook site.yml -v

You'll see lines like this while the task runs:

ASYNC POLL on host1: jid=..., started=1, finished=0
ASYNC POLL on host1: jid=..., started=1, finished=0
ASYNC OK on host1: jid=..., finished=1, rc=0

finished=1, rc=0 means success. Still timing out? Increase retries and rerun. If it's timing out immediately, double-check that your async value isn't the bottleneck.

You can also inspect the job directly on the remote host โ€” Ansible writes async job files here:

ls ~/.ansible_async/
cat ~/.ansible_async/<jid>

Quick Reference: async vs poll vs async_status

  • async: N โ€” Hard kill timer on the remote host. The process dies after N seconds, done or not.
  • poll: 0 โ€” Fire and forget. You're responsible for collecting results with async_status.
  • poll: N (N > 0) โ€” Ansible polls automatically every N seconds. No async_status needed.
  • retries ร— delay โ€” Must be โ‰ฅ async or your polling loop will time out first.

Tips

  • Only use poll: 0 when you actually need parallel execution โ€” running multiple slow tasks simultaneously across hosts, or doing other work while waiting. Otherwise, built-in polling is cleaner.
  • Save the job ID right after launching the async task. If the play crashes before async_status runs, you can still check the job manually on the remote host using the jid from ~/.ansible_async/.
  • Got "failed": true from async_status? That's not a timeout โ€” the task finished but returned a non-zero exit code. Check job_result.stderr for the actual error.
  • Ansible Tower / AWX users: Tower has its own job timeout setting separate from the async value. A task can be killed by Tower before the remote async timer even triggers. Make sure both are configured with enough headroom.

Related Error Notes