Next.js Caching Strategies Explained Simply

Next.js has four caching layers — and most developers only understand one. This guide demystifies them all with practical examples and a clear decision framework.
The Cache Nobody Warned You About
When you first build with Next.js, caching feels simple. Add a revalidate option here, mark something as dynamic there, move on. Then production arrives, and so do the problems.
"Why is my menu still showing last week's prices?" "The booking widget says we're full — but we're not." "My client says their homepage hero was updated two days ago and it's still showing the old one."
These are caching bugs — and they're remarkably common, because Next.js doesn't have one cache. It has four. They operate at different layers, interact in ways that aren't obvious, and each one can independently serve stale data if you haven't thought it through.
This guide breaks down all four caching layers clearly, explains when each one helps or hurts, and gives you a framework for making intentional decisions rather than accidental ones.
The Four Caching Layers in Next.js App Router
Since the App Router was introduced, Next.js has four distinct caching mechanisms:
- Request Memoisation — deduplicated fetch calls within a single render
- Data Cache — persistent server-side cache of
fetch()responses - Full Route Cache — pre-rendered HTML stored on the server or CDN
- Router Cache — client-side cache of visited route segments
They operate at different levels of the stack and have different lifetimes. Let's go through each one.
1. Request Memoisation
What it is: During a single server render, if multiple components fetch the same URL, Next.js only makes one actual HTTP request. The rest are served from memory.
How it works: It's automatic — you don't configure anything. It uses React's extended fetch() API. Any component in the same render tree that calls the same URL gets the cached response.
// All three result in ONE network request during a single render
const a = await fetch('https://api.example.com/menu')
const b = await fetch('https://api.example.com/menu')
const c = await fetch('https://api.example.com/menu')Lifetime: Single render only. Once the page is sent, it's gone.
When it helps: When you've structured your app so multiple components each fetch their own data independently — which is the recommended pattern in the App Router. You don't have to prop-drill or create a global data context; just fetch where you need it, and Next.js handles deduplication automatically.
The catch: This only works with the native fetch() API. Third-party HTTP clients like Axios don't participate in this cache. If you're mixing fetch patterns across your codebase, some requests won't benefit.
2. Data Cache
What it is: A persistent server-side cache for fetch() responses. Unlike request memoisation, this survives across multiple requests and — on Vercel — across deployments.
The default behaviour: Every fetch() call in a Server Component is cached indefinitely unless you say otherwise. This is the one that catches most developers off guard.
// Cached indefinitely — the default (you may not want this!)
const data = await fetch('/api/products')
// Revalidate every hour
const data = await fetch('/api/products', {
next: { revalidate: 3600 }
})
// Always fetch fresh — opt out of data cache entirely
const data = await fetch('/api/products', {
cache: 'no-store'
})
// Tag it for on-demand revalidation
const data = await fetch('/api/products', {
next: { tags: ['products'] }
})Tag-based revalidation is the pattern we recommend most often. Tag your fetches, then call revalidateTag('products') from a Server Action or Route Handler when the underlying data changes — via a CMS webhook, an admin save action, or a payment confirmation event.
When it hurts: Any data that changes more frequently than your cache period, or where accuracy is critical. Booking availability, payment status, personalised content — these should use cache: 'no-store'. We've seen hospitality clients lose real bookings because availability data was cached for 60 seconds and appeared erroneously full during a busy Saturday evening.
3. Full Route Cache
What it is: At build time, Next.js pre-renders routes that use only static data and stores the resulting HTML and React Server Component payload. When a user requests that route, they're served the stored result directly — often from a CDN edge node, before the request even reaches your server.
What makes a route static vs dynamic? Next.js determines this automatically. A route is dynamic if it uses:
cookies()orheaders()fromnext/headerssearchParamsin a page componentfetch()withcache: 'no-store'- The export
dynamic = 'force-dynamic'
Everything else is eligible for static pre-rendering.
When it helps enormously: Homepage, about page, blog posts, restaurant menus, hotel room descriptions, gallery pages, contact pages. For a typical hospitality site, 70–80% of the content doesn't need to be dynamic — and serving these routes from the CDN edge means sub-100ms response times globally.
The common mistake: Developers unnecessarily opt out of static rendering because they're unsure whether something should be dynamic. A restaurant's lunch menu page does not need to be dynamic. Render it at build time, cache it on the CDN, and serve it at edge speed. We helped a hotel client identify twelve routes that had been force-dynamic without any reason — switching them to static reduced their server costs and cut average response times by over 60%.
// Only add this when you genuinely need request-time data
export const dynamic = 'force-dynamic'4. Router Cache
What it is: The browser-side cache that stores route segments the user has visited or that Next.js has pre-fetched in the background. Navigation between pages can feel instant because the data is already in memory.
How it works: When a <Link> component enters the viewport, Next.js pre-fetches the route's data in the background. Once you navigate to a route, its payload is stored in memory for the duration of the session.
Default durations: - Static routes: 5 minutes - Dynamic routes: 30 seconds
When it helps: It makes navigation feel instant, which is especially noticeable on content-rich sites — restaurant menus, hotel gallery pages, multi-step booking flows. From the user's perspective, the site feels like a native application.
When it creates problems: A user who loaded your restaurant's special offers page 4 minutes ago will see the cached version, even if the offer has since expired. For most content this is fine. For anything price-sensitive or time-sensitive — a flash sale, a limited-availability offer, a dynamic pricing table — it's worth thinking about.
Refreshing the router cache:
import { useRouter } from 'next/navigation'
const router = useRouter()
router.refresh() // Forces a fresh fetch for the current routeCalling revalidatePath() from a Server Action also invalidates both the Data Cache and the Router Cache for the affected route.
How the Four Layers Work Together
A real request moves through all four caches in sequence:
- Browser checks the Router Cache — if valid, serve immediately
- Cache miss → request hits the server or CDN, which checks the Full Route Cache
- Cache miss → Server Component runs, calling
fetch()which checks the Data Cache - Cache miss → actual network request, which benefits from Request Memoisation if the same URL is needed elsewhere in the same render
This pipeline is why getting caching right matters so much. A well-configured Next.js application can serve the vast majority of requests without ever touching the database or making an external API call. A misconfigured one either hammers your database unnecessarily or serves stale data — and often both at once.
A Practical Decision Framework
Content that almost never changes
Blog posts, static pages, team pages, legal pages — anything where updates happen at most a few times a month.
Strategy: Let the Full Route Cache do its job. Use indefinite Data Cache and trigger revalidation via a webhook when content changes. These pages should load from the CDN edge in under 100ms.
Content that changes occasionally
Restaurant menus, hotel room descriptions, pricing pages, product catalogues.
Strategy: Data Cache with time-based revalidation (revalidate: 3600) or, better, tag-based revalidation triggered when an admin saves changes. Keeps content fresh without constant database load.
Content that changes frequently
Special offers, event listings, blog post comment counts, inventory levels.
Strategy: Short revalidate window (60–300 seconds) combined with on-demand revalidation where possible. Accept that data may be slightly stale, but bound the staleness to an acceptable window.
Real-time or user-specific content
Booking availability, cart contents, payment status, personalised dashboards.
Strategy: cache: 'no-store' on all fetches, dynamic = 'force-dynamic' on the route. Always fresh, at the cost of higher server load. This is the right trade-off — accuracy is non-negotiable for transactional data.
Key Takeaways
- The default caches aggressively. Every
fetch()call in a Server Component is cached indefinitely unless you opt out. If you don't know what caching strategy each of your fetches is using, you're flying blind. - Static rendering is fast and essentially free — don't opt out without a reason. A restaurant menu page that pre-renders at build time and serves from the CDN costs nothing per request and loads in milliseconds.
- Tag-based revalidation beats time-based for most content. Revalidate when data actually changes, not on an arbitrary schedule. It's more accurate and avoids unnecessary cache misses.
- Stale data isn't always harmless. In hospitality and e-commerce, a cached booking widget or stale price has real business consequences. Know which data needs to be always-fresh and protect it with
cache: 'no-store'. - **When in doubt, start with
cache: 'no-store'.** Verify correctness first, then introduce caching incrementally once you understand the data's update frequency.
Getting This Right From the Start
Caching bugs are uniquely insidious because they don't throw errors. They manifest as data that seems slightly off — until a guest tries to book a table that was already taken, or a customer applies a coupon that expired three days ago, or a hotel manager starts receiving complaints about a promotion that ended last week.
We've reviewed dozens of Next.js applications where the caching configuration was an unintentional mix of defaults, copied snippets, and reactive hotfixes. The result is invariably a system that's harder to reason about and harder to debug than it needs to be — and one that will surprise you in production at the worst possible moment.
We work with hospitality businesses, SaaS platforms, and professional services companies who need their Next.js applications to be both fast and correct. Getting the caching layer right from day one is one of the highest-leverage decisions you can make — and it's one of the areas where experienced developers add the most value over well-intentioned generalists.
If you're building a new Next.js application or suspect your current one has caching problems lurking beneath the surface, get in touch. We'll tell you what we find.
Need help implementing this?
We build high-performance websites and automate workflows for ambitious brands. Let's talk about how we can help your business grow.
More Articles

Next.js Image Optimisation: The Complete Guide
Master Next.js image optimisation with the Image component, lazy loading, responsive sizing, and format conversion to dramatically improve page speed and Core Web Vitals.

Next.js App Router: A Practical Migration Guide
Still on the Pages Router? Here's how to migrate to Next.js App Router incrementally — without breaking production or rewriting everything at once.