{"record":{"id":"0393d7d0936d098d","repo":"docling-project/docling","slug":"timed-out-waiting-for-task-task-id-after-timeou-0393d7","errorCode":null,"errorMessage":"Timed out waiting for task {task_id} after {timeout:.2f}s.","messagePattern":"Timed out waiting for task (.+?) after (.+?)s\\.","errorType":"exception","errorClass":"TaskTimeoutError","httpStatus":null,"severity":"error","filePath":"docling/service_client/watchers.py","lineNumber":237,"sourceCode":"                raise\n            except Exception as exc:\n                raise ServiceUnavailableError(\n                    \"WebSocket status stream is unavailable.\", detail=str(exc)\n                ) from exc\n\n    def _iter_ws_connection(\n        self, ws_url: str, task_id: str, deadline: float, timeout: float\n    ) -> Iterator[TaskStatusResponse]:\n        with connect(\n            ws_url,\n            open_timeout=self._connect_timeout,\n            close_timeout=self._connect_timeout,\n            additional_headers=self._additional_headers,\n        ) as websocket:\n            while True:\n                remaining = deadline - time.monotonic()\n                if remaining <= 0:\n                    raise TaskTimeoutError(\n                        f\"Timed out waiting for task {task_id} after {timeout:.2f}s.\"\n                    )\n\n                raw_message = websocket.recv(timeout=remaining)\n                envelope = WebsocketMessage.model_validate_json(raw_message)\n                status = _process_ws_envelope(envelope, task_id)\n\n                if status is None:\n                    continue\n\n                yield status\n                if is_terminal_task_status(status):\n                    return\n\n                # Only send \"next\" for UPDATE messages.  The server sends\n                # CONNECTION once before the update loop begins; sending\n                # \"next\" in response to it would queue an extra token that\n                # the server later consumes as a request for a post-terminal","sourceCodeStart":219,"sourceCodeEnd":255,"githubUrl":"https://github.com/docling-project/docling/blob/61d76f1ff3f8428065465889f7b4577da7df704c/docling/service_client/watchers.py#L219-L255","documentation":"TaskTimeoutError raised inside the sync WebSocketWatcher's _iter_ws_connection loop when the monotonic deadline passes before a terminal task status arrives. The deadline is set from the watcher's default timeout or the per-call timeout, and it is checked before every websocket.recv(). It means the WebSocket stayed healthy but the task did not reach a success/failure status in time.","triggerScenarios":"Calling wait_for_terminal()/iter_updates() on a WebSocketWatcher where the docling-serve task runs longer than the configured timeout (default_timeout or the timeout= argument), so remaining = deadline - time.monotonic() drops to <= 0 before is_terminal_task_status(status) is true.","commonSituations":"Large batch conversions or GPU-starved servers exceeding a default timeout; timeout left at library default for big workloads; server queue backlog; slow OCR models on CPU.","solutions":["Pass a larger timeout to the wait call (e.g. wait_for_terminal(task_id, timeout=3600))","Raise the watcher's default timeout when constructing the service client","Check the task status on the server (GET /v1/status/poll/{task_id}) to see whether it eventually finished, then retrieve the result instead of resubmitting","Reduce task size (fewer pages per request) so it completes within the budget"],"exampleFix":"# before\nstatus = watcher.wait_for_terminal(task_id)  # default timeout\n\n# after\nstatus = watcher.wait_for_terminal(task_id, timeout=1800.0)","handlingStrategy":"retry","validationCode":"# Estimate needed timeout from document size before waiting\nestimated = max(default_timeout, num_pages * seconds_per_page)\nstatus = watcher.wait_for_terminal(task_id, timeout=estimated)","typeGuard":null,"tryCatchPattern":"from docling.service_client.exceptions import TaskTimeoutError\n\ntry:\n    status = watcher.wait_for_terminal(task_id, timeout=1800)\nexcept TaskTimeoutError:\n    # Task may still finish server-side; check once without resubmitting\n    status = poll_status_fn(task_id, 0.0)\n    if not is_terminal_task_status(status):\n        raise","preventionTips":["Size timeouts to the workload (pages x per-page cost), not the default","For batch jobs, set default_timeout high at client construction","Poll by task_id after a timeout instead of resubmitting the conversion","Monitor server queue depth — growing backlog means longer waits"],"tags":["timeout","websocket","task-watcher","service-client"],"backgroundTag":null,"analyzedSha":"61d76f1ff3f8428065465889f7b4577da7df704c","analyzedAt":"2026-08-14T23:53:18.727Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}