#8363·monkeytype

Bug (backend): cacheWithTTL marks cache fresh before fetch resolves — failed fetch serves stale data for full TTL, no in-flight dedup

Author: priyanshu1976Created Aug 26, 2026Updated Aug 26, 2026

Did you clear cache before opening an issue?

  • I have cleared my cache

Is there an existing issue for this?

  • I have searched the existing open and closed issues

Does the issue happen when logged in?

N/A

Does the issue happen when logged out?

N/A (backend source bug)

Does the issue happen in incognito mode when logged in?

N/A

Does the issue happen in incognito mode when logged out?

N/A

Issue details

Current Behavior

cacheWithTTL in backend/src/utils/ttl-cache.ts marks the cache as fresh before the fetch resolves:

typescript
// backend/src/utils/ttl-cache.ts:20-26
return async () => {
  if (lastFetchTime < Date.now() - ttlMs) {
    lastFetchTime = Date.now(); // updated before await completes
    cache = await fn();
  }
  return cache;
};

Consequences:

  1. Failed fetch poisons the cache for the full TTL. If fn() rejects, the rejection propagates to that caller, but lastFetchTime has already been advanced — every subsequent call within the TTL returns stale cached data instead of retrying. This utility backs the PSA endpoint (controllers/psa.ts), so one failed upstream fetch serves stale content for the whole TTL window.
  2. No in-flight promise dedup. When the TTL expires under concurrent requests, all callers run fn() simultaneously (thundering herd) since nothing records the pending promise.

Expected Behavior

Only advance lastFetchTime after a successful fetch, and dedupe concurrent calls by caching the promise itself:

typescript
let lastFetchTime = 0;
let cache: T | undefined;
let inflight: Promise<T> | undefined;

return async () => {
  if (lastFetchTime < Date.now() - ttlMs) {
    inflight ??= fn()
      .then((result) => {
        cache = result;
        lastFetchTime = Date.now();
        return result;
      })
      .finally(() => {
        inflight = undefined;
      });
    return inflight;
  }
  return cache;
};

Steps To Reproduce

  1. Call a cacheWithTTL-wrapped getter whose fn throws.
  2. Call again within the TTL — stale data is returned with no retry until TTL expiry.

Environment

  • Backend, master @ 91bd24bb8

Source: monkeytypegame/monkeytype