{"record":{"id":"b0fab570c2fc96c0","repo":"invoke-ai/InvokeAI","slug":"timed-out-waiting-to-decode-frames-from-video-pat","errorCode":null,"errorMessage":"Timed out waiting to decode frames from {video_path}","messagePattern":"Timed out waiting to decode frames from (.+?)","errorType":"exception","errorClass":"TimeoutError","httpStatus":null,"severity":"error","filePath":"invokeai/app/util/video_thumbnails.py","lineNumber":355,"sourceCode":"        stderr_reader.join(timeout=1)\n        proc.stderr.close()\n\n\ndef iter_video_frames(\n    video_path: Path,\n    timeout: float = VIDEO_DECODE_TIMEOUT_SECONDS,\n    is_canceled: Optional[Callable[[], bool]] = None,\n) -> Iterator[np.ndarray]:\n    acquired: list[threading.BoundedSemaphore] = []\n    capacity_deadline = time.monotonic() + timeout\n    try:\n        for slot in (_VIDEO_STREAM_SLOTS, _VIDEO_DECODER_SLOTS):\n            while True:\n                if is_canceled is not None and is_canceled():\n                    raise CanceledException\n                remaining = capacity_deadline - time.monotonic()\n                if remaining <= 0:\n                    raise TimeoutError(f\"Timed out waiting to decode frames from {video_path}\")\n                if slot.acquire(timeout=min(0.1, remaining)):\n                    acquired.append(slot)\n                    break\n        # Charge the capacity wait against the same deadline as the first frame, the way\n        # _run_worker does. Handing the decoder a fresh full timeout here would let a\n        # caller that waited just under `timeout` for a slot block for nearly 2 * timeout\n        # before failing — twice the bound the callers (upload probing, node decodes)\n        # believe they are enforcing. Later frames still get a full `timeout` each: after\n        # the first frame the budget is an inactivity bound, not a queueing one.\n        yield from _iter_video_frames_unbounded(\n            video_path,\n            timeout,\n            is_canceled,\n            first_frame_timeout=max(0.0, capacity_deadline - time.monotonic()),\n        )\n    finally:\n        for slot in reversed(acquired):\n            slot.release()","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/invoke-ai/InvokeAI/blob/0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06/invokeai/app/util/video_thumbnails.py#L337-L373","documentation":"Raised by iter_video_frames when it cannot acquire the streaming capacity slots (_VIDEO_STREAM_SLOTS then _VIDEO_DECODER_SLOTS) within the capacity deadline, which is shared with the first-frame timeout. This bounds total concurrent streamers+decoders; the shared deadline prevents waiting ~2x timeout for slot plus first frame. Cancellation is checked while waiting.","triggerScenarios":"Calling iter_video_frames while the maximum number of concurrent stream and decoder slots are held for longer than `timeout`; heavy concurrent thumbnail/frame extraction load; slots leaked by a stuck earlier job; timeout set too small relative to queue depth.","commonSituations":"Burst uploads to a small server saturating decoder slots; long-running stream consumers holding stream slots; load tests opening many iter_video_frames generators at once without closing them (unclosed generators never release slots).","solutions":["Ensure generators are fully consumed or explicitly closed (gen.close() / context management) so slots are released promptly.","Raise the timeout to tolerate queueing, or serialize video jobs through an application-level queue.","Reduce concurrent callers; the slot limits are intentional back-pressure, so scale workers/machines instead.","If slots appear permanently stuck, look for leaked generator objects or hung worker processes holding the semaphores."],"exampleFix":"// before\ngen = iter_video_frames(path)\nfirst = next(gen)  # TimeoutError waiting for slots under load\n# gen abandoned → slot leaked\n// after\nwith contextlib.closing(iter_video_frames(path, timeout=120)) as gen:\n    for frame in gen:\n        process(frame)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    gen = iter_video_frames(path, timeout=120)\n    for frame in gen:\n        process(frame)\nexcept TimeoutError as e:\n    if \"waiting to decode frames\" in str(e):\n        backoff_and_requeue(path)  # pool saturated, retry later\nfinally:\n    gen.close()","preventionTips":["Always close generators (try/finally or contextlib.closing)","Queue video work instead of unbounded parallel streaming","Keep timeout >= expected slot-wait + first-frame decode","Scale decoder capacity with upload throughput"],"tags":["video","timeout","concurrency","backpressure","semaphore"],"backgroundTag":"resource-pool-exhausted","analyzedSha":"0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06","analyzedAt":"2026-08-29T04:46:49.967Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}