calesthio/OpenMontage · error · TimeoutError

HeyGen execution {execution_id} timed out after {timeout}s

Error message

HeyGen execution {execution_id} timed out after {timeout}s

What it means

Raised as TimeoutError when the HeyGen execution poller exhausts its timeout budget (poll interval grows from a base via *1.2 backoff, capped at 30s) without the status becoming completed/failed/error. HeyGen renders can simply take longer than the configured timeout, especially for long multi-scene videos, so this is a client-side give-up, not a server-side failure.

Source

Thrown at tools/video/_shared.py:415

        data = response.json().get("data", {})
        status = data.get("status", "")

        if status == "completed":
            video_url = (
                data.get("output", {}).get("video", {}).get("video_url")
                or data.get("output", {}).get("video_url")
            )
            if video_url:
                return video_url
            raise RuntimeError(f"Completed but no video_url in output: {data}")

        if status in {"failed", "error"}:
            raise RuntimeError(f"HeyGen generation failed: {data.get('error', 'Unknown')}")

        time.sleep(min(interval, max(0.0, deadline - time.time())))
        interval = min(interval * 1.2, 30.0)

    raise TimeoutError(f"HeyGen execution {execution_id} timed out after {timeout}s")


def upload_image_fal(image_path: str) -> str:
    """Upload a local image to fal.ai storage and return a public URL."""
    import requests

    api_key = os.environ.get("FAL_KEY") or os.environ.get("FAL_AI_API_KEY")
    if not api_key:
        raise RuntimeError("FAL_KEY or FAL_AI_API_KEY required for image upload")

    path = Path(image_path)
    if not path.exists():
        raise FileNotFoundError(f"Image not found: {image_path}")

    suffix = path.suffix.lower()
    content_type = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "webp": "image/webp"}.get(
        suffix.lstrip("."), "image/png"
    )

View on GitHub (pinned to 95e1c3d0ab)

Solutions

  1. Increase the timeout parameter passed to the poll/exec call — HeyGen Video Agent jobs routinely need 10+ minutes
  2. Before assuming failure, GET the execution once more by id: it often completed after the caller gave up; reuse that URL instead of resubmitting (and paying) again
  3. Reduce scene count / video length if timeouts are budget-constrained
  4. Wrap with retry that first checks execution status by id, resubmitting only if actually failed

Example fix

# before
video_url = wait_for_video(execution_id, timeout=180)
# after
video_url = wait_for_video(execution_id, timeout=900)  # 15 min for long jobs
Defensive patterns

Strategy: retry

Validate before calling

null

Try / catch

try:
    url = wait_for_video(execution_id, timeout=900)
except TimeoutError:
    # poll once more by id: job frequently completed after give-up
    data = fetch_execution(execution_id)
    if data.get("status") == "completed":
        return extract_video_url(data)
    raise

Prevention

When it happens

Trigger: Rendering a multi-minute multi-scene video with a short timeout; HeyGen under heavy load with slow queue times; conservative timeout defaults (often far below real render durations for Video Agent jobs).

Common situations: Prompt-to-video agent jobs that take 5-15+ minutes while the caller allowed 2-3; peak-load days; 4K or many-scene compositions.

Understand the failure class

Related errors


AI-assisted analysis of calesthio/OpenMontage@95e1c3d0ab (2026-08-15). Data as JSON: /api/errors/826586203e0508e9. Report an issue: GitHub.