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.