{"record":{"id":"f163a245e1840118","repo":"santifer/career-ops","slug":"apify-run-runid-did-not-finish-within-math-ro","errorCode":null,"errorMessage":"Apify run ${runId} did not finish within ${Math.round(timeoutMs / 1000)}s${suffix}","messagePattern":"Apify run (.+?) did not finish within (.+?)s(.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/apify/_apify.mjs","lineNumber":169,"sourceCode":"        url,\n        { headers: authHeaders(token) },\n        Math.min(PER_REQUEST_TIMEOUT_MS, remainingMs),\n      );\n      const run = body?.data;\n      if (run && TERMINAL_STATUSES.has(run.status)) return run;\n      lastError = undefined;\n    } catch (err) {\n      // 4xx (401/403 auth revoked, 404 run not found) won't succeed on retry.\n      if (err?.status >= 400 && err.status < 500) throw err;\n      lastError = err;\n    }\n    const sleepMs = Math.min(POLL_INTERVAL_MS, deadline - Date.now());\n    if (sleepMs > 0) await sleep(sleepMs);\n  }\n  // Fire-and-forget cleanup; don't add abortRun's 5s to our wall-clock budget.\n  void abortRun(runId, token).catch(() => {});\n  const suffix = lastError ? ` (last error: ${lastError.message})` : '';\n  throw new Error(`Apify run ${runId} did not finish within ${Math.round(timeoutMs / 1000)}s${suffix}`);\n}\n\nasync function fetchDatasetItems(runId, token, deadline = null) {\n  const url = `${APIFY_API_BASE}/actor-runs/${runId}/dataset/items`;\n  const items = await fetchJson(\n    url,\n    { headers: authHeaders(token) },\n    PER_REQUEST_TIMEOUT_MS * 2,\n    CONNECT_RETRY_ATTEMPTS,\n    deadline,\n  );\n  if (!Array.isArray(items)) {\n    throw new Error(`Apify run ${runId} returned non-array dataset payload`);\n  }\n  return items;\n}\n\nexport async function runActor(actorId, input, { timeoutMs = DEFAULT_RUN_TIMEOUT_MS, token = process.env.APIFY_TOKEN } = {}) {","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/plugins/apify/_apify.mjs#L151-L187","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst items = await runActorAndWait('owner/actor', { timeoutMs: 60_000 });\n// after\nconst items = await runActorAndWait('owner/actor', { timeoutMs: 600_000 });","handlingStrategy":"retry","validationCode":"const POLL_INTERVAL_MS = 2000, POLL_TIMEOUT_MS = 600_000;\nfunction validateRunBudget(timeoutMs = POLL_TIMEOUT_MS) {\n  if (timeoutMs < POLL_INTERVAL_MS) {\n    throw new Error('timeoutMs must exceed the poll interval');\n  }\n}","typeGuard":"function isTerminalRunStatus(status) {\n  return ['SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT'].includes(status);\n}","tryCatchPattern":"try {\n  const runId = await waitForRun(actorId, input, { timeoutMs: 600_000 });\n} catch (err) {\n  if (/did not finish within/.test(String(err.message))) {\n    // extract runId from the message and re-check the run status out-of-band\n    const runId = String(err.message).match(/run ([^ ]+)/)?.[1];\n    if (runId) await checkRunStatusLater(runId);\n  } else {\n    throw err;\n  }\n}","preventionTips":["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."],"tags":["apify","timeout","polling","async"],"backgroundTag":"request-timeout","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}