← ALL FIELD NOTES

LOG 015 · TECHNICAL · 2023-03-01

A stale-while-revalidate cache took SSR pages from 2-5 seconds to 100ms

2 min read

Situation

Goodpods is a podcast discovery platform, and its Next.js webapp server-side renders podcast and episode pages so search engines can index them. Those SSR requests hit the API live and took 2 to 5 seconds each, which made the pages slow for both crawlers and visitors. With so many unique pages, this was a real SEO problem.

Task

I designed and built the caching layer to make those responses fast, without ever showing stale personalised data to logged-in users.

Action

I wrote an infinite_cache decorator for the API. It serves the cached response from Redis instantly, then fires a background task after the response is sent to regenerate the entry. That is stale-while-revalidate: the user gets speed now, the cache gets freshness a moment later.

The key decision was limiting who could receive cached responses. I gated the decorator to the Next.js SSR and crawler client, detected by user agent, so logged-in users always hit the live path and could never see stale personalised data.

Freshness could not rely on background refreshes alone, so I hooked cache reloads into the places data actually changes: podcast refresh, episode list updates, and review details. I also built a standalone worker that continuously walked every podcast in the database to keep the cache warm, using threading to speed up regeneration.

I treated the worker as production infrastructure, not a script. It ran under supervisor with an admin monitoring endpoint, Slack monitoring, and a kill-switch environment variable, and regeneration shipped off by default behind config so I could enable it deliberately.

Three months later I found that the force-refresh endpoint and the warming script computed different Redis keys, so some refreshes were not landing in the cache the endpoint read. I fixed both to use the same keys.

Result

SSR response times went from 2 to 5 seconds down to about 100ms. The footprint was real: the nginx cache reached around 100 GB because we had so many unique pages. The kill switch and config gating meant the system could roll out and be operated safely, and it kept serving Goodpods’ SEO pages after launch.