nexu-io/open-design · error · Error
grok video timed out after ${elapsedSec}s waiting for status
Error message
grok video timed out after ${elapsedSec}s waiting for status=done (last status: ${lastStatus || 'pending'}, ceiling ${ceilingSec}s). If your jobs legitimately need longer, raise OD_GROK_VIDEO_MAX_POLL_MS. What it means
Thrown when the Grok polling loop exits its window (startedAt + maxMs) without videoUrl being set and without hitting a failed/expired terminal status. maxMs defaults per mode (i2v vs t2v) and is env-overridable via OD_GROK_VIDEO_MAX_POLL_MS. The message reports elapsed seconds, last observed status, and the ceiling so the operator can decide whether to raise the limit. Distinct from [476] (definitive failure) and [478] (no id to poll at all).
Source
Thrown at apps/daemon/src/media/index.ts:2406
}
if (lastStatus === 'done' || lastStatus === 'succeeded') {
videoUrl = pollData?.video?.url || null;
break;
}
if (lastStatus === 'failed' || lastStatus === 'expired') {
const reasonRaw = pollData?.error?.message || pollData?.error || lastStatus;
const reason = typeof reasonRaw === 'string' ? reasonRaw : JSON.stringify(reasonRaw);
throw new Error(`grok task ${lastStatus}: ${reason}`);
}
}
// Loop exited without a videoUrl. Distinguish the two reachable
// cases so operators know which lever to pull: bumping the poll
// ceiling (timeout) vs filing a bug against the upstream contract
// (status=done but no video.url).
if (!videoUrl) {
const elapsedSec = Math.round((Date.now() - startedAt) / 1000);
const ceilingSec = Math.round(maxMs / 1000);
throw new Error(
`grok video timed out after ${elapsedSec}s waiting for status=done `
+ `(last status: ${lastStatus || 'pending'}, ceiling ${ceilingSec}s). `
+ `If your jobs legitimately need longer, raise OD_GROK_VIDEO_MAX_POLL_MS.`,
);
}
}
if (!videoUrl) {
// Submit returned neither an inline video.url nor a request_id —
// upstream broke its own contract. Surfacing the last status helps
// pinpoint whether it was a transient API blip or a malformed
// response we should add a parser branch for.
throw new Error(
`grok video submit returned no inline video and no request_id to poll `
+ `(status=${lastStatus || 'unknown'})`,
);
}
View on GitHub (pinned to 5be4028344)
Solutions
- Raise the ceiling: set OD_GROK_VIDEO_MAX_POLL_MS higher in the daemon environment if your jobs legitimately run long.
- Reduce requested duration or pick a faster path (t2v vs i2v) so xAI finishes within the default window.
- Check xAI status/dashboard — confirm the job actually stalled vs. is still processing; retry if it was queue backlog.
- If lastStatus shows a value that isn't 'done'/'succeeded' but also isn't failed/expired, inspect whether xAI added a new success status the parser should treat as complete.
Example fix
// before // OD_GROK_VIDEO_MAX_POLL_MS unset → default ceiling per mode // after (operator) export OD_GROK_VIDEO_MAX_POLL_MS=1800000 # restart the daemon so the env var is picked up
Defensive patterns
Strategy: retry
Validate before calling
// Before invoking renderGrokVideo, sanity-check the configured ceiling against
// the requested job size so callers get a fast config error instead of a long
// timeout.
function assertGrokCeilingOk(mode: 't2v' | 'i2v', requestedLengthSec: number) {
const maxMs = Number(process.env.OD_GROK_VIDEO_MAX_POLL_MS)
|| (mode === 'i2v' ? 12 * 60 * 1000 : 8 * 60 * 1000);
const estimatedMs = requestedLengthSec * 45 * 1000; // ~45s/s, conservative for Grok
if (estimatedMs > maxMs) {
throw new Error(
`requested ${requestedLengthSec}s grok ${mode} likely exceeds OD_GROK_VIDEO_MAX_POLL_MS (${Math.round(maxMs / 1000)}s); raise the env var or shorten the request.`,
);
}
} Try / catch
// Catch the timeout specifically and surface the raise-the-ceiling hint to the user.
try {
await renderGrokVideo(ctx, creds, onProgress);
} catch (e) {
const msg = String((e as Error).message || e);
if (msg.startsWith('grok video timed out')) {
throw new Error(`Grok video generation timed out. Ask the operator to raise OD_GROK_VIDEO_MAX_POLL_MS. Detail: ${msg}`);
}
throw e;
} Prevention
- Set OD_GROK_VIDEO_MAX_POLL_MS explicitly based on your typical job length; do not rely on the per-mode default.
- Track p95 job duration per mode in observability and alert before it approaches the ceiling.
- If lastStatus at timeout is a value other than pending/done/succeeded/failed/expired, suspect parser drift on a new xAI status string.
When it happens
Trigger: Grok video generation where status stays pending/processing inside the poll loop for longer than maxMs (default mode-dependent, capped). Triggered when the while-loop exits with videoUrl still null and lastStatus never reached 'done'/'succeeded' nor 'failed'/'expired'.
Common situations: Long Grok video jobs that legitimately exceed the default ceiling; xAI queue backlog; default ceiling too low for the requested duration/complexity; status string xAI returns doesn't match the 'done'/'succeeded' matcher so the loop never breaks.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- openrouter video timed out after ${elapsedSec}s waiting for
- grok video non-JSON: ${truncate(submitText, 200)}
- no xAI credentials — sign in with your SuperGrok subscriptio
- grok image ${resp.status}: ${truncate(text, 240)}
- grok image non-JSON: ${truncate(text, 200)}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/ce3ac806262936dc.
Report an issue: GitHub.