n8n-io/n8n · error · Error

Run timed out after ${String(config.timeoutMs)}ms

Error message

Run timed out after ${String(config.timeoutMs)}ms

What it means

Thrown in the run-resumed loop of the chat driver after a run-finish, when the main agent has started a NEW run (run-start count exceeds run-finish count) and the total elapsed time since config.startTime has exceeded config.timeoutMs. Before throwing, the run is cancelled via client.cancelRun (errors swallowed). This guards the 'agent resumed itself' branch so a self-perpetuating loop cannot run forever.

Source

Thrown at packages/@n8n/instance-ai/evaluations/harness/chat-loop.ts:146

		// Wait for observational-memory jobs (observer/reflector) before the next user turn
		await waitForMemoryTasks(config);

		// Check if the main agent started a new run after background tasks completed
		await delay(SSE_SETTLE_DELAY_MS);
		const newRunStarts = countEvents(config.events, 'run-start');
		const currentRunFinishes = countEvents(config.events, 'run-finish');
		if (newRunStarts <= currentRunFinishes) {
			break;
		}

		config.logger.verbose(
			`[${config.threadId}] Main agent resumed (run-start #${String(newRunStarts)}) -- waiting for completion`,
		);

		if (Date.now() - config.startTime > config.timeoutMs) {
			await config.client.cancelRun(config.threadId).catch(() => {});
			throw new Error(`Run timed out after ${String(config.timeoutMs)}ms`);
		}
	}
}

async function waitForRunFinish(config: WaitConfig, expectedFinishCount: number): Promise<void> {
	while (countEvents(config.events, 'run-finish') <= expectedFinishCount) {
		const elapsed = Date.now() - config.startTime;
		if (elapsed > config.timeoutMs) {
			await config.client.cancelRun(config.threadId).catch(() => {});
			throw new Error(`Run timed out after ${String(config.timeoutMs)}ms`);
		}

		await processConfirmationRequests(config);
		await delay(POLL_INTERVAL_MS);
	}
}

async function waitForBackgroundTasks(config: WaitConfig, timeoutMs: number): Promise<void> {

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Raise config.timeoutMs for the case if the run legitimately needs more time.
  2. Investigate why the agent resumes (new run-start) repeatedly — a tool loop or repeated confirmation requests.
  3. If transient, re-run the case; persistent timeouts indicate an agent-behavior or backend-liveness problem.
Defensive patterns

Strategy: try-catch

Validate before calling

// Budget check before resuming — surface a clear reason instead of a raw timeout.
if (Date.now() - config.startTime > config.timeoutMs) {
  throw new Error(`Aborting before resume: elapsed ${Date.now() - config.startTime}ms exceeds budget ${config.timeoutMs}ms; raise config.timeoutMs or investigate the resume loop.`);
}

Try / catch

// Treat timeout as a recoverable eval failure, not a crash.
try {
  await driveChatLoop(config);
} catch (e) {
  if (e instanceof Error && /Run timed out after/.test(e.message)) {
    recordCaseTimeout(config.threadId, config.timeoutMs);
  }
  throw e;
}

Prevention

When it happens

Trigger: A multi-turn case where the agent keeps starting new runs (e.g. it loops on a tool, or background tasks trigger follow-up runs) such that the wall-clock budget runs out while the loop waits for the resumed run.

Common situations: An agent caught in a tool-call loop; an overly generous timeoutMs still exceeded by a pathological case; slow LLM responses across many resume cycles.

Understand the failure class

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/aa3452da854561b5. Report an issue: GitHub.