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

  1. Increase the wait timeout (timeoutMs / deadline passed to the plugin call) to cover the actor's real runtime.
  2. Check the run on Apify console / API (`GET /v2/actor-runs/{runId}`) to see its actual status and any failure reason.
  3. Reduce actor workload (fewer input items, cheaper memory settings) so it finishes within the budget.
  4. If the suffix shows a lastError, fix that underlying polling issue (token/network) — the run may have finished but polling could not confirm it.
  5. 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

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.

Related errors


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)