Ask a room of experienced Next.js developers what the hardest part of the framework is, and caching wins. Not server components, not the routing, not the build. Caching. And the strange thing is that no single piece of it is conceptually difficult. The difficulty is that the caching is largely invisible, the defaults are surprising and have changed repeatedly, and when something goes wrong the symptom rarely points at the cause. You see stale data. You do not see that caching is why, because you never knowingly turned it on.
The problem is that you did not opt in
Most caching bugs in Next.js are stale data from a cache you did not know was active. That is the heart of it. In a normal mental model, caching is something you add: you decide a thing should be cached, you cache it, and when it is stale you know exactly where to look. Next.js inverts that. Caching happened by default, at layers you did not touch, and so when the data is wrong your first instinct is to debug your data fetching, your database, your API, everything except the cache you never wrote.
This is made worse by there being not one cache but several, each at a different level, interacting. There is request-level memoization that dedupes fetches within a single render. There is a persistent data cache on the server. There is a full route cache that stores entire rendered pages. There is a client-side router cache that serves pages from memory during navigation. Put a CDN in front and there is another layer again. Each one is reasonable on its own. Stacked together and mostly invisible, they produce a situation where "why is this showing old content" can have five different answers at five different levels, and nothing on screen tells you which.
The defaults keep changing, so what you know is probably wrong
Here is the part that makes it genuinely treacherous rather than just hard. The caching defaults have changed across major versions, in opposite directions, which means the knowledge you carefully built is now actively misleading you.
Older Next.js cached aggressively by default. A plain fetch was cached indefinitely unless you opted out, which caused exactly the stale-data bugs everyone complained about. So the framework reversed course: in newer versions, fetch is no longer cached by default, route handlers are dynamic by default, and the client router cache no longer holds page segments the way it used to. The official reasoning was explicit, that implicit caching caused too many "why isn't my data updating" bugs, and explicit is better than implicit. More recently the direction moved again toward an explicit opt-in model where you mark what should be cached deliberately.
The practical consequence is brutal for anyone maintaining real projects. The caching behaviour of your app depends on which version you are on, and advice written for one version is wrong for another. A developer who learned "Next caches fetches by default" and a developer who learned "Next never caches fetches by default" are both right, for different versions, and both will be confidently wrong the moment they touch a project on the other one. You cannot rely on memory here. You have to know your version's actual behaviour.
Development lies to you
On top of all this, caching does not behave the same in development as in production. Pages are rendered on demand in dev so you can see your changes, which means the caching that will bite you in production often does not appear while you are building. You test locally, everything updates instantly, you ship, and the stale data shows up in production where the real caching is active. The only reliable way to see what your users will see is to run a production build locally and test against that, not against the dev server that has quietly disabled the thing you needed to test.
How to actually stay sane
The way out is not to memorise every layer. It is to change your default posture from trusting the framework's caching to being explicit about it.
Decide caching deliberately, per piece of data, based on how often it changes and how personalised it is. Static configuration that never changes can cache hard. Content that changes occasionally should use time-based or tag-based revalidation. User-specific data should not be cached at all. When you make that an explicit choice rather than inheriting a default, the behaviour stops surprising you.
For content from a CMS, lean on tag-based revalidation. In a Sanity build, the clean pattern is to tag the data you fetch and have a webhook trigger revalidateTag when an editor publishes, so the cache clears exactly when the content actually changed and not on a timer you guessed at. That turns "editor published an hour ago and the site still shows the old version" from a mystery into a wired-up, predictable flow.
And when you are debugging stale data, make the cache your first suspect, not your last. The instinct is to check everything else first because you do not remember adding a cache. Reverse that instinct. In Next.js, the cache you did not write is the most likely culprit, and "which layer is holding this" is usually the fastest question to a fix. The framework's caching is powerful, and on a well-configured site it is a big part of why the thing is fast. It is just powerful in a way that defaults to invisible, and the entire skill is dragging it back into the light and deciding, on purpose, what should be cached and for how long.


