Why is my cache retry logic sometimes returning stale data after a refetch?

Hey folks, I’m wiring up a tiny fetch wrapper for a web app and I’m trying to keep it simple: cache responses, retry on flaky networks, and dedupe in-flight requests. The failure mode I keep seeing is the UI briefly showing old data even after the refetch resolves, which feels like a race I’m accidentally creating.

const cache = new Map();
const inflight = new Map();

export async function getJSON(url, { ttlMs = 5000, retries = 2 } = {}) {
  const now = Date.now();
  const hit = cache.get(url);
  if (hit && now - hit.t < ttlMs) return hit.v;

  if (inflight.has(url)) return inflight.get(url);

  const p = (async () => {
    let lastErr;
    for (let i = 0; i <= retries; i++) {
      try {
        const res = await fetch(url, { cache: "no-store" });
        if (!res.ok) throw new Error(res.status);
        const v = await res.json();
        cache.set(url, { t: Date.now(), v });
        return v;
      } catch (e) {
        lastErr = e;
      }
    }
    throw lastErr;
  })().finally(() => inflight.delete(url));

  inflight.set(url, p);
  return p;
}

What’s the safest way to structure this so a late retry or an older in-flight request can’t overwrite newer cached data and cause that stale flash?

Sarah :grinning_face_with_smiling_eyes:

You’re seeing the flash because an earlier fetch (or a retry) can finish after a newer one and still cache.set the old payload, so the last writer wins even if it’s stale. Add a per-URL request id and only write to the cache if the id still matches when the response arrives.

Sora