Learn from basic caching strategies on the web to caching strategies in Next.js.

Have you tried turning on developer tools and refreshing the website?
Looking at the Network tab, some requests return 200, others return 304. Even if I force refresh (Ctrl+Shift+F5), I see x-vercel-cache: HIT pop up and think, "What is this again?" Have you ever wanted to?
When developing with Next.js, these questions sometimes arise. “I obviously modified the data, but why isn’t it reflected on the screen?”
Is caching really a necessary evil? Or is it because I don't understand it properly?
Let's assume you've created a simple product listing page with Next.js 15.
export default async function ProductsPage() {
const response = await fetch('https://api.mystore.com/products');
const products = await response.json();
return (
<div>
<h1>Product List</h1>
{products.map((product) => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>{product.price}won</p>
</div>
))}
</div>
);
}
In a development environment (npm run dev), changes to API data are reflected immediately on each refresh.
But in production (npm run build && npm run start)... the changes are not reflected even if you refresh.
There is an important point here. The default behavior of fetch changed in Next.js 15.
“Then why doesn’t the data change if caching doesn’t work in v15?”
Starting with these questions, we'll slowly dive into the intricacies of Next.js caching. Just to mention, fetch caching is just one of four caching mechanisms that Next.js has.
First, let's experiment with how the browser caches.
You've probably noticed these differences:
304 Not Modified200 OK status304 Not Modified means the server is saying “It’s the same version you have, use that!”
Browser: "I have this file, version 2025-08-24"
Server: "Checked it, nothing changed. I'll return 304."
Browser: "Then I'll use it from the cache."
At this time, no actual data is transmitted. This is why the Size in the Network tab is as small as 0.1KB.
What would happen if there was no caching on the web?
User: Naver logo please (3MB)
Server: Naver logo delivery (3MB transfer)
User: (0.1 seconds later) Please give me the Naver logo again.
Server: Naver logo delivered (also 3MB transferred)
How inefficient would it be to transmit like this every time?
Caching is similar to convenience stores
The pros and cons of each level are listed below.
| URL | Status | Size |
|---|---|---|
| /logo.png | 200 | 3.2MB |
| /style.css | 200 | 45KB |
| /script.js | 200 | 128KB |
304 Not Modified: 브라우저 캐시를 사용합니다. 실제 데이터는 전송되지 않아 Size가 작습니다.
As you can see in the simulator above, a 304 response involves significantly less data transfer. But how does the server know that the file hasn't changed?
Browsers and servers check file versions in two ways:
First request:
Server → Browser: “Last-Modified: 2025-08-24 10:00:00”
Re-request:
Browser → Server: “If-Modified-Since: 2025-08-24 10:00:00”
Server: (Check file modification time) "304 Not Modified"
First request:
Server → Browser: "ETag: abc123" (hash value of file contents)
Re-request:
Browser → Server: “If-None-Match: abc123”
Server: (Check current file hash) "304 Not Modified"
Now let's go back to the first question. What is the reason why x-vercel-cache: HIT appears even when I force refresh?”
x-vercel-cache value in Response HeadersWhat did you discover?
HIT: fetched from cacheMISS: not in cache, retrieved from origin serverSTALE: It is old, but provided for now, being updated in the background.Forced refresh only ignores browser cache. But there is another cache layer between the browser and the server. It is CDN (Content Delivery Network).
[Browser] ← Ignore force refresh only here → [CDN cache] ← → [Next.js server]
↑
x-vercel-cache: HIT
What if 100 Korean users request the same page from a US server at the same time?
Without CDN 100 people → Across the Pacific → US server (processed 100 times)
If you have a CDN 1 person → Across the Pacific → US server → Save to Seoul CDN Remaining 99 people → Seoul CDN responds immediately
Now, let’s take a closer look at Next.js’ caching system. Next.js provides a total of 4 caching mechanisms.
Deduplicates identical requests in the same rendering cycle.
// What if these components are on the same page?
async function Header() {
const user = await fetch('/api/user'); // first call
// ...
}
async function Sidebar() {
const user = await fetch('/api/user'); // duplication! (0ms, using cache)
// ...
}
async function MainContent() {
const user = await fetch('/api/user'); // duplication! (0ms, using cache)
// ...
}
The characteristics of Request Memoization are as follows.
// Next.js 15 default: no caching
const data = await fetch('https://api.example.com/data');
// Explicitly enable caching
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache', // or next: { revalidate: 3600 }
});
The features of Data Cache are as follows.
// This page is built statically
export default async function StaticPage() {
const data = await fetch('...', { cache: 'force-cache' });
return <div>{data}</div>;
}
// This page is dynamic (rendered on every request)
export default async function DynamicPage() {
const data = await fetch('...', { cache: 'no-store' });
return <div>{data}</div>;
}
The features of Full Route Cache are as follows.
// Pages moved to the link are cached in the browser memory.
<Link href="/about">About</Link>
// Why does it load immediately when you go back?
The features of Router Cache are as follows.
Let's say you're creating a product listing page. Think about the characteristics of each data.
상품명, 가격, 설명 등
실시간 재고 현황
The situation is more complicated this time. Both real-time and performance must be considered.
캐싱 전략에 따라 사용자가 보는 가격이 어떻게 달라지는지 확인해보세요
Is caching always good? That's not true.
revalidate in Next.js works in an interesting way.
// revalidate: 60 = update every 60 seconds
const data = await fetch('/api/data', {
next: { revalidate: 60 },
});
How will it behave after 60 seconds?
13:00:00 - User 1 connects → Cache creation
13:00:30 - User 2 connects → Use cache (fresh)
13:01:00 - User 3 connects → uses cache (60 seconds have passed, stale state)
→ Start importing new data in the background at the same time
13:01:01 - User 4 connects → uses new cache
User 3 receives outdated data but receives an immediate response. This is the core of SWR.
Suitable for:
If not suitable:
// Page level: static generation
export default async function ProductPage({ params }) {
// Product information: long caching
const product = await fetch(`/api/products/${params.id}`, {
next: { revalidate: 3600 },
});
// Review: Medium Caching
const reviews = await fetch(`/api/products/${params.id}/reviews`, {
next: { revalidate: 300 },
});
return (
<>
<ProductInfo data={product} />
<Reviews data={reviews} />
<StockStatus productId={params.id} /> {/* Client component */}
</>
);
}
// Client component: real-time data
('use client');
function StockStatus({ productId }) {
// Real-time inventory is processed by the client
const { data } = useSWR(`/api/stock/${productId}`, fetcher, {
refreshInterval: 5000,
});
return <div>Inventory: {data?.quantity} units</div>;
}
// Invalidate cache with server action
async function updateProduct(productId: string, data: any) {
await db.products.update({ id: productId, ...data });
// Invalidate specific tags
revalidateTag(`product-${productId}`);
// or invalidate a specific path
revalidatePath(`/products/${productId}`);
}
// ❌ Bad example: caching user-specific data
const userData = await fetch('/api/me', {
next: { revalidate: 3600 }, // Same data for other users?
});
// ✅ Good example: do not cache user-specific data
const userData = await fetch('/api/me', {
cache: 'no-store',
});
// ❌ Bad example: Associative data cache inconsistency
const product = await fetch(`/api/products/${id}`, {
next: { revalidate: 3600 },
});
const category = await fetch(`/api/categories/${product.categoryId}`, {
next: { revalidate: 86400 }, // What if the category changes?
});
// ✅ Good example: synchronizing associative data
const [product, category] = await Promise.all([
fetch(`/api/products/${id}`, { next: { tags: [`product-${id}`] } }),
fetch(`/api/categories/${categoryId}`, {
next: { tags: [`category-${categoryId}`] },
}),
]);
Caching is not a panacea. You need an appropriate strategy for your situation.
Understand the characteristics of the data
Put user experience first
Measure and Improve
// v14: Default caching (explicitly turned off by developer)
// v15: Default no-cache (explicitly turned on by developer)
// Now you need to clearly express your intentions
const cachedData = await fetch('/api/data', {
cache: 'force-cache', // “I want caching”
});
This change is a decision that prioritizes “predictable behavior.” The philosophy is that it is safer to explicitly enable it when needed, rather than causing chaos due to caching.