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