koala73/worldmonitor · error
Crypto gap cache unavailable
Error message
Crypto gap cache unavailable
What it means
Inside the coalesced cache fetcher, the code re-reads the gap-quotes cache key and, if readCachedJson reports status 'error' (the cache backend itself is unreadable), throws 'Crypto gap cache unavailable'. This deliberately aborts the fetcher so an unhealthy cache cannot trigger paid third-party quote fetches (cachedFetchJson treats read errors as misses, which would otherwise cause paid API work).
Solutions
- Check the cache backend (Upstash Redis) health and credentials (REST token/URL) in the environment
- Retry the request; if the cache read recovers, the fetcher proceeds normally
- If cache reads keep failing, the intent is to fail closed rather than spend paid quote API calls — restore cache connectivity first
- Monitor readCachedJson error rates and alert on cache-backend failures to catch this before users do
Example fix
// before
if ((await readCachedJson(cacheKey)).status === 'error') {
throw new Error('Crypto gap cache unavailable');
}
// after
const cacheRead = await readCachedJson(cacheKey);
if (cacheRead.status === 'error') {
console.error('[list-crypto-quotes] cache read failed for', cacheKey, cacheRead.error);
throw new Error('Crypto gap cache unavailable');
} Defensive patterns
Strategy: retry
Validate before calling
// check cache reachability before issuing quote requests
const healthy = await readCachedJson('health:ping').then(r => r.status !== 'error').catch(() => false);
if (!healthy) console.warn('quote cache backend unhealthy'); Type guard
function isCacheHit(r: { status: string; value?: unknown }): r is { status: 'ok'; value: unknown } {
return r.status === 'ok';
} Try / catch
try {
const quotes = await listCryptoQuotes(ids);
} catch (e) {
if (e.message === 'Crypto gap cache unavailable') {
await backoffRetry(() => listCryptoQuotes(ids), { attempts: 3, baseMs: 1000 });
} else throw e;
} Prevention
- Monitor Redis/Upstash health and auth token expiry proactively
- Alert on readCachedJson error-rate spikes
- Understand this is a deliberate fail-closed guard against paid API spend — fix the cache, don't bypass it
When it happens
Trigger: listCryptoQuotes requests gap quotes while the Redis/Upstash cache read for cacheKey fails — connection error, auth failure, or timeout on the cache backend — inside the GAP_CACHE_TTL fetcher.
Common situations: Upstash Redis outage or credentials rotated/revoked, VPC/firewall blocking the cache endpoint, cache client misconfigured in the environment, or transient cache network blips coinciding with high crypto-quote demand.
Related errors
- cache_all_null
- cache_all_null
- Failed to persist resilience score generation for ${generati
- PortWatch cache read returned incomplete results
- PortWatch cache read failed
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/f1e56c03f7c4a2c9.
Report an issue: GitHub.
Appendix: source
Thrown at server/worldmonitor/market/v1/list-crypto-quotes.ts:250
let provider: 'seed' | 'upstream' | 'mixed' | 'degraded' =
gapIds.length === 0 ? 'seed' : (seedHits.length > 0 ? 'mixed' : 'upstream');
if (gapIds.length > 0 && !seedUnavailable) {
// Bounded upstream fetch, Redis-cached per sorted gap set. cacheFetcherErrors
// is false so provider failures rethrow and never write a negative cache.
// An empty provider result is treated as a negative (120s NEG_SENTINEL) via
// a `null` fetcher return, never as a positive 600s `{}` cache entry.
const cacheKey = `market:crypto:gap:v2:${await sha256Hex([...gapIds].sort().join(','))}`;
try {
const cached = await cachedFetchJson<Record<string, CryptoQuote>>(
cacheKey,
GAP_CACHE_TTL,
async () => {
// cachedFetchJson treats read errors as misses. Recheck inside its
// coalesced fetcher so an unreadable cache cannot trigger paid work.
if ((await readCachedJson(cacheKey)).status === 'error') {
throw new Error('Crypto gap cache unavailable');
}
const got = await fetchGapQuotes(gapIds);
return got.size > 0 ? Object.fromEntries(got) : null;
},
120,
{ timeoutMs: 15_000, cacheFetcherErrors: false },
);
if (cached) {
for (const [id, quote] of Object.entries(cached)) resolved.set(id, quote);
}
} catch (err) {
// sentry-coverage-ok: a provider/upstream failure degrades to explicit unresolvedIds + the 3s local backoff; never a hidden drop or poisoned cache.
console.warn('[crypto-quotes] upstream gap fetch failed:', (err as Error).message);
}
// Any gap id the provider could not resolve gets a relay attempt (separate
// egress IP). Relay results are applied per-request and not Redis-cached.
const stillMissing = gapIds.filter((id) => !resolved.has(id));View on GitHub (pinned to 7d06c8633d)