{"record":{"id":"8604e0c0e3c68c31","repo":"koala73/worldmonitor","slug":"redis-request-failed","errorCode":null,"errorMessage":"Redis request failed","messagePattern":"Redis request failed","errorType":"exception","errorClass":null,"httpStatus":503,"severity":"critical","filePath":"api/health.js","lineNumber":2630,"sourceCode":"        : [],\n      checkedAt: new Date().toISOString(),\n    };\n    return new Response(JSON.stringify(body, null, 2), { status: 200, headers });\n  }\n\n  // A snapshot hit is one Redis command instead of the ~390-command registry\n  // sweep below. A failed snapshot read is a real Redis outage, not a cache\n  // miss: returning 503 preserves UptimeRobot's hard-down signal.\n  let refreshLockToken = null;\n  let ownsSnapshotRefreshLock = false;\n  try {\n    if (!getRedisCredentials()) throw new Error('Redis not configured');\n    // Read the snapshot this request will actually render. `?compact=1` — the\n    // browser poll, ~115k/day — reads the ~1 KB compact key instead of dragging the\n    // full ~20 KB check map out of Redis to show a tenth of it (#5300).\n    const snapshotKey = compact ? HEALTH_VERDICT_COMPACT_SNAPSHOT_KEY : HEALTH_VERDICT_SNAPSHOT_KEY;\n    const snapshotResult = await redisPipeline([['GET', snapshotKey]], 4_000);\n    if (!snapshotResult) throw new Error('Redis request failed');\n    if (snapshotResult[0]?.error) throw new Error('Redis snapshot read failed');\n    const cachedSnapshot = parseHealthVerdictSnapshot(snapshotResult[0]?.result, snapshotNow(), { requireChecks: !compact });\n    // Activation deadlines are exact to the second, so the 60s verdict cache\n    // must not outlive either rollout grace. A snapshot written just before a\n    // deadline would otherwise keep serving a softened verdict for up to a\n    // minute after strictness was supposed to begin. Sweep fresh instead.\n    if (cachedSnapshot && !hasExpiredActivationGrace(cachedSnapshot, snapshotNow())) {\n      return healthResponse(cachedSnapshot, compact, headers);\n    }\n\n    refreshLockToken = `${now}:${crypto.randomUUID()}`;\n    let lockResult = await redisPipeline([[\n      'SET',\n      HEALTH_VERDICT_REFRESH_LOCK_KEY,\n      refreshLockToken,\n      'EX',\n      String(HEALTH_VERDICT_REFRESH_LOCK_TTL_SECONDS),\n      'NX',","sourceCodeStart":2612,"sourceCodeEnd":2648,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/health.js#L2612-L2648","documentation":"Thrown on the health snapshot read path when redisPipeline([['GET', snapshotKey]]) returns null. redisPipeline returns null on missing credentials, non-2xx HTTP, fetch timeout (4s here), or a response body that is not an array of the expected length. Because a failed snapshot read is treated as a real Redis outage rather than a cache miss, this triggers a 503 REDIS_DOWN.","triggerScenarios":"GET /api/health (or ?compact=1) reaches api/health.js:2630 with credentials present (so error [0] did not fire) but the Upstash pipeline GET failed — Upstash returned 5xx, the fetch timed out at 4s, or the response body was malformed/not an array.","commonSituations":"Upstash regional outage or degraded latency pushing the pipeline over the 4s budget; an invalid/expired token causing a 401 (resp.ok false → null); a transient network blip between Vercel Edge and Upstash.","solutions":["Confirm the Upstash database is up and reachable via the Upstash console.","Verify UPSTASH_REDIS_REST_TOKEN is current (a rotated token returns 401, which redisPipeline maps to null).","Check Upstash latency/timeout metrics — if pipelines routinely approach 4s, the snapshot read needs a larger budget or the DB plan needs scaling.","Retry the health request; transient nulls surface as a single 503 but the next snapshot read usually succeeds."],"exampleFix":"// before\nconst snapshotResult = await redisPipeline([['GET', snapshotKey]], 4_000);\nif (!snapshotResult) throw new Error('Redis request failed');\n// after — surface the specific failure reason for diagnostics\n// (handle in redisPipeline itself by returning a tagged failure rather than null)","handlingStrategy":"retry","validationCode":"import { getRedisCredentials, redisPipeline } from './_upstash-json.js';\n\nasync function redisSnapshotReadable(): Promise<boolean> {\n  if (!getRedisCredentials()) return false;\n  const r = await redisPipeline([['PING']], 4_000);\n  return r !== null && !r[0]?.error;\n}","typeGuard":null,"tryCatchPattern":"// health.js already wraps the snapshot read in a try/catch that returns 503\n// REDIS_DOWN. Callers (UptimeRobot) should treat 503 as hard-down and alert.\n// In application code that reuses redisPipeline:\ntry {\n  const result = await redisPipeline([['GET', key]], 4_000);\n  if (!result) throw new Error('Redis request failed');\n  // ...\n} catch (err) {\n  // Distinguish credentials-missing (config fix) from null-on-outage (retry).\n}","preventionTips":["Distinguish 'Redis not configured' (config) from 'Redis request failed' (outage/timeout) — only the latter is retryable.","Monitor Upstash latency; a pipeline that routinely nears the 4s budget will flap into null.","Keep the Upstash token current; a revoked token returns 401 which redisPipeline maps to null."],"tags":["redis","health","upstash","network","timeout"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}