docling-project/docling · warning · TaskTimeoutError

Timed out waiting for task {task_id} to emit status updates.

Error message

Timed out waiting for task {task_id} to emit status updates.

What it means

TaskTimeoutError raised by PollingWatcher.wait_for_terminal when iter_updates yielded zero updates — the final_status is still None. This happens when the very first poll consumes/exceeds the whole deadline (e.g. server-side long-poll blocks until timeout) so the generator never yields before the deadline check fires.

Source

Thrown at docling/service_client/watchers.py:142

            # 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)

    def wait_for_terminal(
        self, task_id: str, timeout: float | None = None
    ) -> TaskStatusResponse:
        final_status: TaskStatusResponse | None = None
        for update in self.iter_updates(task_id=task_id, timeout=timeout):
            final_status = update

        if final_status is None:
            raise TaskTimeoutError(
                f"Timed out waiting for task {task_id} to emit status updates."
            )
        return final_status


class WebSocketWatcher:
    """Status watcher using `WS /v1/status/ws/{task_id}` with poll fallback."""

    def __init__(
        self,
        ws_url_for_task: Callable[[str], str],
        poll_fallback: PollingWatcher | None,
        fallback_to_poll: bool,
        connect_timeout: float,
        default_timeout: float,
        additional_headers: dict[str, str] | None = None,
    ) -> None:
        self._ws_url_for_task = ws_url_for_task

View on GitHub (pinned to 61d76f1ff3)

Solutions

  1. Use a timeout comfortably larger than poll_server_wait (one full poll cycle plus margin)
  2. Omit the timeout to use the client default, which is sized for normal polling
  3. If it recurs with sane timeouts, check whether the service's long-poll endpoint is hanging
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)
except TaskTimeoutError as exc:
    if 'emit status updates' in str(exc):
        status = watcher.wait_for_terminal(task_id, timeout=timeout_s * 3)

Prevention

When it happens

Trigger: wait_for_terminal with a timeout smaller than one server long-poll cycle (poll_server_wait), so the first _poll_status consumes the entire budget and the loop exits with no yielded update.

Common situations: Passing a very small explicit timeout (a few seconds) against a server configured with long poll waits; tight deadline arithmetic where remaining <= 0 right after the first poll returns.

Understand the failure class

Related errors


AI-assisted analysis of docling-project/docling@61d76f1ff3 (2026-08-14). Data as JSON: /api/errors/0e1e5ecb90d34613. Report an issue: GitHub.