Why your deploy isn't showing: a field guide to the five caches between you and your users
You shipped, the pipeline is green, and the site still shows last week. Before you blame the framework, meet the five caches standing between your commit and your customer — and the 5-minute triage that names the guilty one.
You shipped. The pipeline is green, the server says the new commit is live, and the homepage still shows last week. Nothing in software erodes trust in a deploy pipeline faster — and in nine cases out of ten the pipeline is innocent. Between your commit and your customer stand up to five caches, each with its own idea of “fresh”, and any one of them can serve the past with total confidence.
We got to relive this recently on our own site. We updated content, rebuilt, verified the build succeeded — and the old text survived. We rebuilt again. It survived again. Two clean builds, one stubborn ghost. The culprit turned out to be a cache we will introduce as suspect number four, and the fix was one deliberate rm in the right place. That evening produced this field guide.
Meet the five suspects
1. The browser. Your own machine is the least trustworthy witness in the room. Hard reloads lie too, because service workers and memory cache have opinions. Always confirm from a fresh private window — better, from curl, which has no memory and no feelings.
2. The CDN or reverse proxy. Anything with Cache-Control headers and an edge node can hold HTML hostage. The response headers confess: look for age, x-cache, or a suspiciously old last-modified.
3. The prerender. Static generation bakes your pages at build time. If content changed in the database after the build, the baked HTML is legitimately stale until the next revalidation window — that is a feature wearing a bug costume.
4. The framework's data cache. The quiet one. Next.js, for example, persists cached data reads on disk in .next/cache — and that directory survives rebuilds. Our ghost lived here: the new build happily prerendered pages using week-old database reads it found in the cache it inherited. Two rebuilds, same ghost, because we rebuilt the code and kept the memories.
5. The database layer. Materialized views, query caches, replicas catching up. Rare on small sites, first place to look when only some data is stale.
The 5-minute triage
Work from the origin outwards — it is faster than guessing from the browser inwards.
Minute 1:curl the page directly on the origin server (bypass the CDN, hit localhost or the internal port). New content there? Then the app is fine — suspects 1–2, go clear the edge and stop reading.
Minute 2: stale on origin too? Check whether the route is prerendered. If the page has a revalidation window, wait it out or trigger revalidation — suspect 3, working as designed; decide whether the design is right.
Minute 3: rebuilt and still stale? Suspect 4. Clear the framework's data cache and rebuild once. If that fixes it, write the eviction into your deploy script so it never happens again — ours now runs rm -rf .next/cache before every build, and the ghost has not returned.
Minutes 4–5: only some values stale, HTML fresh? Suspect 5. Check what sits between your query and the table.
The rule that prevents the whole genre
Every cache you add must come with a written eviction story: what clears it, when, and who owns that. Not a vague “it revalidates hourly” — a sentence a colleague can act on at 2 a.m. If you cannot write the eviction story, you are not adding a cache, you are adopting a stray ghost.
Caches are wonderful. Ours is why the page you are reading arrived in well under a second. But a cache is a promise about the past, and promises need expiry dates. Put the five suspects on a sticky note; the next “deploy isn't showing” costs you five minutes instead of an evening.