koala73/worldmonitor · error

Timeout — scenario worker may be down

Error message

Timeout — scenario worker may be down

What it means

After the polling loop exhausts without receiving a 'done' (or 'failed') status, result is still unset and the code throws this error. It indicates the scenario worker neither completed nor explicitly failed within the polling window, so it may be down, overloaded, or the job was lost.

Solutions

  1. Check that the scenario worker is running and healthy (its health endpoint / deployment status).
  2. Retry the scenario; transient queue congestion often resolves on a second attempt.
  3. Reduce scenario size/complexity so it completes within the polling window.
  4. If the job is stuck, inspect worker logs/queue for the job id and restart the worker if necessary.

Example fix

// before
if (!result) throw new Error('Timeout — scenario worker may be down');
// after
if (!result) {
  const healthy = await fetch(workerHealthUrl).then(r => r.ok).catch(() => false);
  throw new Error(healthy ? 'Scenario timed out; try a smaller scenario' : 'Scenario worker unreachable');
}
Defensive patterns

Strategy: retry

Try / catch

try {
  const result = await runScenario(params);
} catch (e) {
  if (e.message.includes('Timeout')) {
    showBanner('Scenario worker slow or down — retrying…');
    await retryWithBackoff(() => runScenario(params), { attempts: 2 });
  } else throw e;
}

Prevention

When it happens

Trigger: Polling loop ends with result === null — every poll returned a non-terminal status (queued/running) or the loop ran out of iterations before 'done'/'failed' ever arrived.

Common situations: Scenario worker process down or not deployed; worker queue backed up under load making the job slower than the poll budget; network partition between client and status endpoint; very large scenario exceeding the built-in timeout.

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 koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/b8cfe822dd67edae. Report an issue: GitHub.

Appendix: source

Thrown at src/components/SupplyChainPanel.ts:1182

      let result: ScenarioResult | null = null;
      // 60 × 1s = 60s max (worker typically completes in <1s). 1s poll keeps
      // the perceived latency <2s in the common case. First iteration polls
      // immediately (no sleep) in case the worker was already running on a
      // previous job and blocked here only because of network round-trip.
      for (let i = 0; i < 60; i++) {
        if (signal.aborted) { resetButton('idle'); return; }
        if (!this.content.isConnected) return; // panel gone — nothing to update
        if (i > 0) await new Promise(r => setTimeout(r, 1000));
        const status = await getScenarioStatus(jobId, { signal });
        if (status.status === 'done') {
          const r = status.result;
          if (!r || !Array.isArray(r.topImpactCountries)) throw new Error('done without valid result');
          result = r;
          break;
        }
        if (status.status === 'failed') throw new Error('Scenario failed');
      }
      if (!result) throw new Error('Timeout — scenario worker may be down');
      if (signal.aborted) { resetButton('idle'); return; }
      if (!this.content.isConnected) return;
      // After this callback fires, showScenarioSummary() → render() will rebuild
      // the scenario-trigger DOM with the button already in its "Active" +
      // disabled state (driven by activeScenarioState in renderChokepoints()).
      // Do NOT touch the captured btn reference here — it's about to be detached
      // by render()'s setContent(), and any imperative update would no-op
      // silently while the fresh button shows the wrong state.
      this.scenarioRunState.delete(scenarioId);
      this.onScenarioActivate?.(scenarioId, result);
    } catch (err) {
      // Abort from a new click = user-triggered retry, no error banner needed.
      if (err instanceof Error && err.name === 'AbortError') {
        resetButton('idle');
        return;
      }
      console.error('[scenario] run failed:', err);
      resetButton('error');

View on GitHub (pinned to 7d06c8633d)