Next.js 16 made caching explicit. It also made every uncached read a decision you have to defend.
With Cache Components enabled, the old mental model inverts. Pages are static by default, and anything that reads request data or hits the network at request time has to say so, either by opting into a cache with a lifetime or by sitting behind a Suspense boundary.
In practice this is a forcing function. Every data read now needs an answer to one question: can this be stale, and for how long? Marketing content can be stale for an hour. A logged-in dashboard cannot be stale at all. Writing the answer down as a cache lifetime, next to the query, is better documentation than any comment.
The constraint that catches people out is that a cached function cannot read cookies or headers. That restriction follows the call stack, so a helper three levels down that reads a header fails the same way. The fix is to read request data outside the cached scope and pass it in as an argument, which also makes the cache key honest about what the result depends on.
Closures count as arguments. A cached function that captures a variable from an enclosing scope gets that value folded into its cache key. This is usually what you want, but it means putting a cache directive inside a class method is a mistake: the instance comes along for the ride.
Our rule is that cache directives live in module-level query functions that resolve their dependencies from the container inside the function body. Nothing non-serializable is captured, the cache key is exactly the arguments, and the repositories underneath stay plain classes with no framework coupling.