If you’ve recently upgraded to Next.js 15, you might have noticed something... weird. You change data in your database, reload the page, and... nothing. The old data is still there. You hard refresh. Still there. You verify the database update. It’s definitely changed. So why is Next.js serving you stale content? Welcome to the brave new world of aggressive caching. While this is great for performance, it can be a nightmare for development if you don't know how to tame it. If you are also migrating to v16, you might find our Next.js 16 Migration Guide helpful as well.
It’s not just you. Caching is notoriously one of the hardest things to get right in web development. Next.js 15 introduces a highly aggressive caching strategy by default to ensure your site is blazing fast. While this is great for performance, it can be a nightmare for Next.js troubleshooting when you actually need fresh data.
In this guide, we are going to demystify the caching layers in Next.js 15. We will walk through exactly why your data is stale and, more importantly, how to fix it fast. Whether you are building a blog, a dashboard, or an e-commerce store, mastering these concepts is essential for Next.js performance.
1. Introduction: Understanding Next.js 15 Caching
Before we start throwing code at the problem, we need to understand the beast we are dealing with. Why is Next.js caching so much stuff?
The philosophy behind Next.js 15 is simple: cache everything possible to reduce server costs and improve speed.
When a user visits your site, Next.js doesn't want to rebuild the page from scratch or hit your database every single time. Instead, it stores copies of the work it has already done. Think of it like a chef in a restaurant. If 50 people order the "Daily Special" soup, the chef doesn't chop carrots and boil water 50 separate times. They make a huge pot in the morning (cache it) and ladle it out instantly.
However, Next.js 15 doesn't just have one "pot" of soup. It has four distinct caching layers that work together:
- Request Memoization: This happens on the server during a single request. If you call the same data-fetching function twice in one component tree, Next.js only executes it once.
- Data Cache: This is persistent. When you make a
fetchrequest, Next.js stores the result on the server. Even if you redeploy, this cache might persist depending on your hosting provider. - Full Route Cache: This caches the HTML and React Server Component payload. This is what makes static pages instant.
- Router Cache (Client-side): This lives in the user's browser. It stores previously visited routes so that navigating back and forth feels instant.
Most Next.js caching issues arise because developers fix one layer (like the Data Cache) but forget about another (like the Router Cache).
2. Common Caching Scenarios in Next.js 15
Let's look at the most common scenarios where caching trips up developers. Recognizing these patterns is half the battle in Next.js troubleshooting.
The "Stuck" Static Page
You build a page that fetches a list of blog posts. You add a new post in your CMS, but your live site doesn't show it. This happens because, by default, Next.js tries to make pages Static. If your data fetch doesn't specify that it's dynamic, Next.js builds the HTML once at build time and never looks back.
The API That Never Updates
You are using fetch to get data from an external API. The API data changes frequently, but your app keeps showing the same JSON response from three days ago. This is the Data Cache at work. In Next.js App Router, fetch requests are cached indefinitely by default unless you tell them otherwise.
The Client-Side "Ghost" Data
You have a "Delete" button. You click it, the item vanishes, and you redirect the user back to the list. But wait... the deleted item is still there! Then you hit refresh, and it's gone. This is the Router Cache (client-side) holding onto the old view to make navigation snappy.
Understanding which scenario you are in helps you choose the right weapon to fix it.
3. Debugging Data Cache Issues
The most frequent culprit for stale data is the Data Cache. Next.js extends the native fetch API to allow you to configure how requests are cached.
If you are seeing old data, your first step is to inspect your fetch calls.
The Default Behavior
In Next.js 15, this simple fetch call is cached forever:
// This request is cached indefinitely by default
async function getData() {
const res = await fetch('https://api.example.com/products');
return res.json();
}
If you are coming from standard React or older frameworks, this is confusing. Usually, fetch just fetches. But here, Next.js intercepts it and checks its persistent cache first.
Solution A: Opting Out of Caching
If you need real-time data (like live stock prices or user-specific notifications), you should disable caching entirely for that request. We use cache: 'no-store'.
async function getData() {
const res = await fetch('https://api.example.com/products', {
cache: 'no-store' // Don't cache this! Fetch every time.
});
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
}
This tells Next.js: "Hey, never save this. Always go to the source." This ensures fresh data but might slow down your page since it waits for the API every time.
Solution B: Time-Based Revalidation (ISR)
Often, you don't need live data, just reasonably fresh data. If you run a news site, updating the content every 60 seconds is fine. This is where Next.js data fetching shines with Incremental Static Regeneration (ISR).
async function getData() {
const res = await fetch('https://api.example.com/news', {
next: { revalidate: 60 } // Cache for 60 seconds, then update.
});
return res.json();
}
With revalidate: 60, the first user to visit after 60 seconds triggers a background update. Everyone else gets the cached version instantly. It’s the perfect balance between speed and freshness.
If you are pulling data from a database directly (using an ORM like Prisma) instead of fetch, you won't have the fetch options. In that case, you can export a route segment config:
// Add this to your page.js or layout.js
export const revalidate = 60; // Revalidate this page every 60 seconds
This applies to all data fetching in that route segment.
4. Invalidating the Full Route Cache
When you modify data (like submitting a form to update a user's profile), you must tell Next.js to purge the cache for that data. You do this using revalidatePath or revalidateTag inside your Server Action. If you are new to this pattern, read our Server Actions Guide.
Using revalidatePath
The revalidatePath function allows you to purge cached data for a specific path. This is incredibly useful in form submissions (Server Actions).
Imagine a Server Action that updates a user's profile:
'use server'
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function updateProfile(formData) {
const name = formData.get('name');
// 1. Mutate data in your database
await db.user.update({ where: { id: 1 }, data: { name } });
// 2. Purge the cache for the profile page
revalidatePath('/profile');
// 3. Redirect (optional)
redirect('/profile');
}
When revalidatePath('/profile') runs, Next.js knows the cached HTML for /profile is invalid. The next time the page is requested, it will be regenerated with the new data from the database.
Using revalidateTag
revalidatePath is great, but what if your data appears on 10 different pages? You don't want to call revalidatePath 10 times.
Enter revalidateTag. You can tag your fetch requests with a label, and then invalidate that label globally.
Step 1: Tag your fetch
fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
});
Step 2: Invalidate the tag
'use server'
import { revalidateTag } from 'next/cache';
export async function createPost() {
// ... create post logic ...
// Purge cache for ANY fetch request tagged with 'posts'
revalidateTag('posts');
}
This is a powerful Next.js caching strategy for large applications. It allows for surgical precision when clearing your cache.
5. Client-Side Cache Management
So, you fixed the server cache. You used no-store or revalidatePath. But when you navigate around your app, you still see old data. Why?
Meet the Router Cache.
The Router Cache stores the payload of route segments in the browser for a short period (30 seconds for dynamic routes, 5 minutes for static). This makes clicking the "Back" button instant. However, it can be annoying after a mutation (like submitting a form) if the user is redirected back to a list that hasn't visually updated yet.
The Fix: router.refresh()
If you are performing mutations on the client side (perhaps using a standard onSubmit handler in a client component), you need to tell the Next.js router to refresh the current route.
This requires the useRouter hook.
Here is how to force a client-side refresh:
'use client'
import { useRouter } from 'next/navigation';
export default function DeleteButton({ id }) {
const router = useRouter();
async function handleDelete() {
await fetch(`/api/items/${id}`, { method: 'DELETE' });
// This refreshes the current route, fetching new data from the server
router.refresh();
}
return <button onClick={handleDelete}>Delete Item</button>;
}
Crucial Note: router.refresh() does not clear the browser's history. It simply re-fetches the data for the current view from the server and updates the UI. It ensures that the client-side cache syncs up with your freshly updated server data.
6. Leveraging Cache Control Headers
Sometimes the issue isn't Next.js—it's the browser or a CDN (Content Delivery Network) sitting in front of your site.
If you are seeing stale images or assets, you might need to look at cache control headers. These are instructions sent with your files that tell browsers "keep this for a year" or "check for updates every time."
In Next.js, you can set these headers in your next.config.js for static assets, or within your API routes.
For a dynamic API route in the App Router (route.js), you might want to explicitly prevent browser caching:
import { NextResponse } from 'next/server';
export async function GET() {
const data = { time: Date.now() };
return NextResponse.json(data, {
headers: {
'Cache-Control': 'no-store, max-age=0',
},
});
}
This header no-store, max-age=0 screams at the browser: "Do not save this! Seriously!"
For optimal Next.js performance, you generally want to:
- Cache static assets (images, CSS, JS) aggressively (Long
max-age). - Cache dynamic HTML content moderately or use
must-revalidate. - Never cache private user data (use
no-store).
7. Advanced Caching Strategies
For those building complex applications, simple revalidation might not cut it. Here are two advanced concepts to keep in your toolkit.
Opting out of Static Rendering Dynamic Functions
Next.js tries to be static by default. However, using certain functions switches a route to Dynamic Rendering automatically. These functions are:
cookies()headers()searchParams
If you use cookies() to check for an authentication token, the page becomes dynamic. It will be rendered on every request (server-side rendering), and the Data Cache behavior might change.
If you are debugging why a page is suddenly slow (not cached), check if you accidentally introduced one of these dynamic functions. If you need cookies but also want caching, you have to be very careful about separating your static content from your personalized content (perhaps using Suspense boundaries).
The unstable_cache Function
Next.js provides a helper called unstable_cache (don't let the name scare you, it's widely used but the API might change slightly in major versions). This allows you to cache the result of any expensive database operation or function, not just fetch.
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
const getCachedUser = unstable_cache(
async (id) => db.user.findUnique({ where: { id } }),
['user-data'], // Key parts
{ tags: ['users'], revalidate: 3600 } // Options
);
This effectively gives you the power of the Data Cache for your database queries. If your database queries are slow, wrapping them in unstable_cache can drastically improve your server-side rendering cache performance.
8. Conclusion: Best Practices for Next.js Caching
Next.js 15 caching is powerful, but it requires a mental shift. It's no longer just "request and response." It's about managing state across the server and the client.
Here is your checklist to fix Next.js 15 caching issues fast:
- Identify the Layer: Is the stale data in the Data Cache (server), Route Cache (HTML), or Router Cache (browser)?
- Use
no-storefor Real-Time Data: If it must be fresh, disable the cache on thefetchcall. - Use Tags for Groups: Group related data fetches with
tagsand invalidate them together usingrevalidateTag. - Don't Forget the Client: Use
router.refresh()after mutations to update the current view. - Check Your Logs: Next.js logging in development mode will often tell you if a route is being statically generated (Circle icon) or server-rendered (Lambda icon).
By following these strategies, you can stop fighting the framework and start using it to build incredibly fast, reliable applications. Caching doesn't have to be a mystery—it just needs a little management.
Frequently Asked Questions (FAQ)
Why is my Next.js page not updating after deployment? Fixing caching issues often feels like playing whack-a-mole, but once you understand the layers—Data Cache, Router Cache, and Full Route Cache—it becomes a manageable science. Start with the aggressive
export const dynamic = 'force-dynamic'to confirm it is indeed a caching issue, then optimize from there. And if you run into hydration mismatches while debugging, check our Hydration Error Debugging Guide.
What is the difference between
revalidatePathandrevalidateTag?revalidatePathclears the cache for a specific URL path (e.g.,/blog/post-1).revalidateTagclears the cache for all fetch requests that share a specific tag (e.g.,posts), regardless of which page they appear on. Tags are generally more flexible for managing data dependencies across multiple pages.
Does
router.refresh()reload the page? No,router.refresh()does not do a full browser refresh. It invalidates the client-side Router Cache and makes a request to the server to get the latest data for the current route, then updates the React components with the new data without losing state like scroll position or input values.
How do I disable caching globally in Next.js 15? While not recommended for performance, you can force a route to be dynamic by adding
export const dynamic = 'force-dynamic'to the top of yourpage.jsorlayout.jsfile. This prevents the Full Route Cache and Data Cache from storing static data for that specific segment.
Can I cache database queries that don't use
fetch? Yes! You can use theunstable_cachefunction imported fromnext/cache. It wraps your database function and allows you to define cache keys, revalidation times, and tags, just like you would with afetchrequest.