← ALL FIELD NOTES

LOG 028 · TECHNICAL · 2023-09-01

Making Next.js caching behave at a million requests a day

2 min read

Situation

The Goodpods webapp, built on Next.js, served over a million requests a day. Podcasters frequently messaged us saying their podcast page had not updated with their newest episodes, and sometimes users got a stale version of the app itself after a deploy. On top of that, Next.js was eating a lot of CPU and memory serving those pages.

Task

We went digging and traced the problems to our caching layers. I took on getting the caching under control: pages should serve fast from cache, refresh in the background, and never get stuck stale.

Action

The behaviour I wanted was stale-while-revalidate. It was fine to serve an old podcast page to one user while reloading it for the next, because that beat a slow load that made users bounce or hurt our SEO ranking. Next.js did not support stale-while-revalidate, and nginx could serve cached pages much faster than Next.js anyway, so I put an nginx cache in front of the app and let nginx handle the revalidation. I centralised the cache-control headers in one place so podcast and episode pages sent consistent s-maxage values. Next.js’s own caching kept getting in the way. Some pages cached forever, so I added explicit fallback rules to stop caching them. Statically generated pages cached for a year and broke things, so I stopped using static pages entirely. Our rsync deploys were corrupting the .next static folder, which I fixed by preserving the static cache folder and excluding it from the sync. The last symptom was stale JS bundles after a deploy. I tried several rounds of cache busting, including adding a build ID to each bundle item, before finding the real fault was a fallback image component. I fixed that component properly and removed the cache-busting hacks.

Result

Podcast and episode pages served straight from the nginx cache while refreshing in the background, at over a million requests a day, with the serving load taken off Next.js. The year-long static caching that caused more harm than good was gone. The cache-busting workarounds came out once the underlying image component was fixed, which left the caching setup simpler than when I started.