docling-project/docling · warning · TaskTimeoutError
Timed out waiting for task {task_id} after {wait_timeout:.2f
Error message
Timed out waiting for task {task_id} after {wait_timeout:.2f}s. What it means
TaskTimeoutError raised by PollingWatcher.iter_updates when the monotonic deadline (default_timeout or the per-call timeout) expires before the task reaches a terminal status. Each poll passes the remaining window to the server's long-poll; once remaining <= 0 the wait fails.
Source
Thrown at docling/service_client/watchers.py:114
poll_client_interval: float | None,
default_timeout: float,
) -> None:
self._poll_status = poll_status
self._poll_server_wait = poll_server_wait
self._poll_client_interval = (
poll_server_wait if poll_client_interval is None else poll_client_interval
)
self._default_timeout = default_timeout
def iter_updates(
self, task_id: str, timeout: float | None = None
) -> Iterator[TaskStatusResponse]:
wait_timeout = self._default_timeout if timeout is None else timeout
deadline = time.monotonic() + wait_timeout
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TaskTimeoutError(
f"Timed out waiting for task {task_id} after {wait_timeout:.2f}s."
)
poll_wait = min(self._poll_server_wait, remaining)
poll_started = time.monotonic()
update = self._poll_status(task_id, poll_wait)
yield update
if is_terminal_task_status(update):
return
# Keep a minimum client-side poll cadence when server-side wait is ignored.
sleep_for = _poll_sleep_duration(
poll_started=poll_started,
poll_interval=self._poll_client_interval,
deadline=deadline,
)
if sleep_for > 0:
time.sleep(sleep_for)View on GitHub (pinned to 61d76f1ff3)
Solutions
- Pass a larger timeout to wait_for_terminal (or raise the client's default) sized to real conversion time
- Check service queue depth/worker count — the task may simply be waiting in queue
- Note the task keeps running server-side: you may re-attach with iter_updates(task_id) instead of resubmitting
Example fix
# before status = watcher.wait_for_terminal(task_id) # default timeout too short # after status = watcher.wait_for_terminal(task_id, timeout=1800.0)
Defensive patterns
Strategy: retry
Type guard
def is_task_timeout(exc: BaseException) -> bool:
return isinstance(exc, TaskTimeoutError) Try / catch
from docling.service_client.exceptions import TaskTimeoutError
try:
status = watcher.wait_for_terminal(task_id, timeout=timeout_s)
except TaskTimeoutError:
status = watcher.wait_for_terminal(task_id, timeout=timeout_s) # re-attach; task still runs Prevention
- Size timeouts to p99 conversion time plus queue wait, not median
- Remember timeout expires client-side only — re-attach rather than resubmit
When it happens
Trigger: wait_for_terminal()/iter_updates() on a task that runs longer than the configured timeout — big documents, queued behind many jobs, or a stuck task that never transitions.
Common situations: Default timeout too small for large batch conversions; overloaded single-worker service; task actually failed to progress (worker crash) and never terminates.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Timed out waiting for task {task_id} to emit status updates.
- Timed out waiting for task {task_id} after {timeout:.2f}s.
- Task {task_id} was not found.
- gRPC infer call failed for model {self.model_name}: {exc}
- Task {task_id} was not found.
AI-assisted analysis of docling-project/docling@61d76f1ff3 (2026-08-14).
Data as JSON: /api/errors/1be6d406197ed0db.
Report an issue: GitHub.