heygen-com/hyperframes · error · StageBudgetTimeoutError

stage budget exceeded while ${label} (${budgetMs}ms)

Error message

stage budget exceeded while ${label} (${budgetMs}ms)

What it means

Thrown by withRemainingBudget() when a capture stage exceeded its allotted wall-clock budget. The function races the work promise against a setTimeout(remainingMs); if the timer fires first (or remainingMs was already <= 0 on entry), it rejects with a StageBudgetTimeoutError carrying the label and budgetMs. This is the mechanism that bounds each capture stage so a hung page does not stall the whole render indefinitely.

Source

Thrown at packages/cli/src/capture/captureTimeout.ts:63

export function isProtocolEvaluateTimeoutError(err: unknown): boolean {
  if (err instanceof TimeoutError) {
    return !hasNavigationTimeoutMessage(err.message);
  }
  return hasProtocolEvaluateTimeoutMessage(errorMessage(err));
}

export function isDegradableEvaluateTimeoutError(err: unknown): boolean {
  return isStageBudgetTimeoutError(err) || isProtocolEvaluateTimeoutError(err);
}

export async function withRemainingBudget<T>(
  work: Promise<T>,
  remainingMs: number,
  label: string,
): Promise<T> {
  if (!(remainingMs > 0)) {
    throw new StageBudgetTimeoutError(label, Math.max(0, remainingMs));
  }

  let timer: ReturnType<typeof setTimeout> | undefined;
  try {
    return await Promise.race([
      work,
      new Promise<never>((_resolve, reject) => {
        timer = setTimeout(() => {
          reject(new StageBudgetTimeoutError(label, remainingMs));
        }, remainingMs);
      }),
    ]);
  } finally {
    if (timer !== undefined) {
      clearTimeout(timer);
    }
  }
}

View on GitHub (pinned to c2996c8626)

Solutions

  1. Increase the stage/capture timeout if the composition legitimately needs more time (pass a larger --timeout or the relevant option).
  2. Profile the composition's in-page script — a hang usually means an infinite loop or a synchronous wait; optimize or make it async.
  3. Ensure external resources the page depends on are local or fast (deterministic rendering forbids render-time network fetches).
  4. If the budget was already <= 0 on entry, an earlier stage overran — investigate the label of the prior stage.

Example fix

// before — default timeout too tight for heavy comp
$ hyperframes render heavy.html
// allow more time
$ hyperframes render heavy.html --timeout 120000
// in composition: avoid blocking the main thread
// before (hangs): while(true){}
// after: yield with requestAnimationFrame
Defensive patterns

Strategy: retry

Validate before calling

// Ensure the stage budget is positive before calling withRemainingBudget.
if (!(remainingMs > 0)) {
  throw new Error(`No budget left for stage '${label}' — increase the capture timeout.`);
}

Try / catch

import { isDegradableEvaluateTimeoutError } from '@hyperframes/cli/capture/captureTimeout';
try {
  await withRemainingBudget(work, remainingMs, label);
} catch (err) {
  if (isDegradableEvaluateTimeoutError(err)) {
    // degrade gracefully: skip optional capture step or lower fidelity
  } else throw err;
}

Prevention

When it happens

Trigger: withRemainingBudget(work, remainingMs, label) is called with a remaining budget that is exhausted either up-front (remainingMs <= 0) or because the work promise did not settle before the setTimeout fired. Typical during in-page script evaluation that hangs (infinite loop, blocked on a resource).

Common situations: Composition runs a heavy in-page animation/JS that blocks the main thread past the stage budget; a network resource the page waits on is slow/blocked; the overall capture timeout is too tight for a complex composition; the page hung on a WebGPU/shader compile.

Related errors


AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12). Data as JSON: /api/errors/16d2d7d2080c6b47. Report an issue: GitHub.