koala73/worldmonitor · error · ApiError

Failed to enqueue scenario job

Error message

Failed to enqueue scenario job

What it means

Thrown as HTTP 502 when the Upstash Redis RPUSH of the scenario job payload produces no usable result: runRedisPipeline returns [] on transport failure, and a missing or non-numeric result means the enqueue never landed. The handler maps this to 502 specifically so callers retry, since the write failed atomically and a retry cannot half-apply.

Source

Thrown at server/worldmonitor/scenario/v1/run-scenario.ts:67

  const depth = typeof depthEntry?.result === 'number' ? depthEntry.result : 0;
  if (depth > MAX_QUEUE_DEPTH) {
    throw new ApiError(429, 'Scenario queue is at capacity, please try again later', '');
  }

  const jobId = generateJobId();
  const payload = JSON.stringify({
    jobId,
    scenarioId,
    iso2: iso2 || null,
    enqueuedAt: Date.now(),
  });

  // Upstash RPUSH returns the new list length; helper returns [] on transport
  // failure. Either no entry or a non-numeric result means the enqueue never
  // landed — surface as 502 so the caller retries.
  const [pushEntry] = await runRedisPipeline([['RPUSH', QUEUE_KEY, payload]], true);
  if (!pushEntry || typeof pushEntry.result !== 'number') {
    throw new ApiError(502, 'Failed to enqueue scenario job', '');
  }

  // statusUrl is a server-computed convenience URL preserved from the legacy
  // /api/scenario/v1/run contract so external callers can keep polling via the
  // response body rather than hardcoding the status path. See the proto comment
  // on RunScenarioResponse for why this matters on a v1 → v1 migration.
  const statusUrl = `/api/scenario/v1/get-scenario-status?jobId=${encodeURIComponent(jobId)}`;

  // Async-enqueue contract: the job is accepted, not complete. Restore the
  // legacy 202 Accepted status (lost in the sebuf migration — the generated
  // server hardcodes 200) and point at the poller via Location. The gateway
  // applies the override on POST-200 only, so the thrown 403/429/502 paths
  // above keep their status.
  setSuccessStatusOverride(ctx.request, 202);
  setResponseHeader(ctx.request, 'Location', statusUrl);

  return {
    jobId,

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Verify UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are set and valid in the deployment environment
  2. Test connectivity directly, e.g. runRedisPipeline([['PING']], true) or curl the Upstash REST endpoint
  3. Retry the RunScenario request — the enqueue never landed, so a retry does not duplicate the job
  4. If persistent, inspect server/_shared/redis.ts runRedisPipeline to see which failure path returns an empty array

Example fix

// before
const res = await fetch(runUrl, { method: 'POST', body });
if (!res.ok) throw new Error('run failed');
// after
const res = await fetch(runUrl, { method: 'POST', body });
if (res.status === 502) {
  // safe to retry: the enqueue never landed
  return retryWithBackoff(() => fetch(runUrl, { method: 'POST', body }));
}
Defensive patterns

Strategy: retry

Try / catch

try { await runScenario(req); } catch (e) { if (e?.status === 502) return retryWithBackoff(() => runScenario(req)); throw e; } — the enqueue never landed, so retry cannot duplicate the job.

Prevention

When it happens

Trigger: RPUSH scenario-queue:pending fails or returns a non-numeric result: Upstash REST endpoint unreachable, missing/invalid UPSTASH_REDIS_REST credentials, Redis outage or timeout, or a response-shape regression in the _shared/redis pipeline helper.

Common situations: UPSTASH_REDIS_REST_URL/TOKEN missing or wrong in the deployment env; Upstash incident or quota exhaustion; egress blocked from the edge runtime; runRedisPipeline change that swallows errors and returns [].

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/986f4b9cc541e782. Report an issue: GitHub.