santifer/career-ops · error · Error
Apify run did not finish within s
Error message
Apify run ${runId} did not finish within ${Math.round(timeoutMs / 1000)}s${suffix} What it means
waitForRun polls an Apify actor run until it reaches a terminal (SUCCEEDED) state or the timeout budget expires. On timeout it fires a fire-and-forget abortRun to stop the actor so credits are not wasted, then throws with the run id, the timeout in seconds, and optionally the last polling error that preceded the timeout.
Solutions
- Increase the wait timeout (timeoutMs / deadline passed to the plugin call) to cover the actor's real runtime.
- Check the run on Apify console / API (`GET /v2/actor-runs/{runId}`) to see its actual status and any failure reason.
- Reduce actor workload (fewer input items, cheaper memory settings) so it finishes within the budget.
- If the suffix shows a lastError, fix that underlying polling issue (token/network) — the run may have finished but polling could not confirm it.
- For huge jobs, run detached: start the run, store the returned runId, and fetch dataset items later instead of blocking on waitForRun.
Example fix
// before
const items = await runActorAndWait('owner/actor', { timeoutMs: 60_000 });
// after
const items = await runActorAndWait('owner/actor', { timeoutMs: 600_000 }); Defensive patterns
Strategy: retry
Validate before calling
const POLL_INTERVAL_MS = 2000, POLL_TIMEOUT_MS = 600_000;
function validateRunBudget(timeoutMs = POLL_TIMEOUT_MS) {
if (timeoutMs < POLL_INTERVAL_MS) {
throw new Error('timeoutMs must exceed the poll interval');
}
} Type guard
function isTerminalRunStatus(status) {
return ['SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT'].includes(status);
} Try / catch
try {
const runId = await waitForRun(actorId, input, { timeoutMs: 600_000 });
} catch (err) {
if (/did not finish within/.test(String(err.message))) {
// extract runId from the message and re-check the run status out-of-band
const runId = String(err.message).match(/run ([^ ]+)/)?.[1];
if (runId) await checkRunStatusLater(runId);
} else {
throw err;
}
} Prevention
- Set timeouts based on measured actor runtime, with generous headroom.
- Prefer detached runs (startRun then poll the dataset later) for long jobs.
- Read the 'last error' suffix — fix polling failures (token/network) rather than only raising the timeout.
- Keep total wall-clock budget in mind: earlier fetch retries share the same deadline.
When it happens
Trigger: Calling waitForRun (directly or via the apify plugin's run-and-wait flow) on an actor whose run takes longer than the configured timeoutMs, or whose run stays in READY/RUNNING indefinitely because it crashed internally without a terminal status, or polling kept failing due to network/token errors until the deadline.
Common situations: Long-running scrapers (large datasets, anti-bot retries) exceeding the default poll timeout; an actor stuck in a retry loop on the Apify side; the polling deadline being shared with earlier fetch attempts so little wall-clock budget remains; Apify platform slowness.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- apify: invalid timeoutMs
- Apify actor finished with status
- Apify did not return a run id
- apify: entry has invalid field_map. Each of title, url…
- apify: entry missing 'actor' (e.g. misceres/indeed-scraper)
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/f163a245e1840118.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/apify/_apify.mjs:169
url,
{ headers: authHeaders(token) },
Math.min(PER_REQUEST_TIMEOUT_MS, remainingMs),
);
const run = body?.data;
if (run && TERMINAL_STATUSES.has(run.status)) return run;
lastError = undefined;
} catch (err) {
// 4xx (401/403 auth revoked, 404 run not found) won't succeed on retry.
if (err?.status >= 400 && err.status < 500) throw err;
lastError = err;
}
const sleepMs = Math.min(POLL_INTERVAL_MS, deadline - Date.now());
if (sleepMs > 0) await sleep(sleepMs);
}
// Fire-and-forget cleanup; don't add abortRun's 5s to our wall-clock budget.
void abortRun(runId, token).catch(() => {});
const suffix = lastError ? ` (last error: ${lastError.message})` : '';
throw new Error(`Apify run ${runId} did not finish within ${Math.round(timeoutMs / 1000)}s${suffix}`);
}
async function fetchDatasetItems(runId, token, deadline = null) {
const url = `${APIFY_API_BASE}/actor-runs/${runId}/dataset/items`;
const items = await fetchJson(
url,
{ headers: authHeaders(token) },
PER_REQUEST_TIMEOUT_MS * 2,
CONNECT_RETRY_ATTEMPTS,
deadline,
);
if (!Array.isArray(items)) {
throw new Error(`Apify run ${runId} returned non-array dataset payload`);
}
return items;
}
export async function runActor(actorId, input, { timeoutMs = DEFAULT_RUN_TIMEOUT_MS, token = process.env.APIFY_TOKEN } = {}) {View on GitHub (pinned to aac998c7ed)