← Statpro dev log

2026-10-12

The Stats home got a cache with a bedtime: 15 seconds fresh, 90 stale

webmobileproduct

The Stats home got a cache with a bedtime: 15 seconds fresh, 90 stale

Yesterday I wrote about Statpro’s scores strip showing every league in one request; the next bottleneck was the page it lives on. The Stats home fires something like nine upstream reads per league before it can render anything, and every member gets a copy assembled fresh for them, even though the page carries no personal data at all. On a live NFL Sunday that means hundreds of phones paying for the same page to be built.

The honest fix: build it once per league and time zone, share it. A cache sounds like a Tuesday-afternoon chore, but the shape of this one is the interesting part. Three properties had to hold at once:

  • Fresh enough for live scores: 15 seconds, because the home carries games in progress.
  • Stale-while-revalidate for 90 seconds: past the fresh window you still get the old copy instantly while one rebuild runs in the background. The common case is a single cache read, never a wait.
  • Never cache a failure: if the build throws, nothing is stored and the next request retries for real.

That third one keeps you honest: a cache that stores errors turns a transient hiccup into a page serving the same crash to everybody for the whole TTL. The actual core, verbatim from the repo since nothing in it is sensitive:

/** Reads the assembled home through the cache; a build failure is never cached. */
export function cachedStatsHome(league, scope, build, cache, forceRefresh = false) {
  return cache(statsHomeCacheKey(league, scope), build, {
    ttl: 15, staleTtl: 90, forceRefresh,
    onCacheState: (state) => recordTimingStep(`home_cache_${state}`, 0),
  });
}

Cache work fails quietly, so every read reports its state (hit, miss, stale) into server timing: you can see the warm path working instead of believing it.

A shared cache has a nasty edge: the page shows today in your time zone, so the key includes both. A warmer that only primes New York helps one city in five. Ours walks five US zones (the contiguous four plus Arizona, which skips DST) times three leagues, one at a time to keep database load flat.

Same idea, different layer: the app now remembers your assembled home between launches and throws it away when the account or the data changes.

this is the build log of statpro · statpro.io · all entries · essays