Until now, we've only worked with "hard coded" components, no external APIs. Why? Because of Next data cache.
Data cache allows the result of external requests (using fetch) to be stored on the server. It also handles revalidation of this data. Data cache means that instead of making a request twice, the second request would just return the cached result of the first one. Of course, you can always opt out and force a fresh request every time.
data cache and full route cache are intertwined. They alter each others behavior, and it can become tricky to know what's happening or who's doing what. The goal of this chapter is to clear the fog.
Cache options
Next extends the native fetch api with options. One of these options is cache:
fetch('url', { cache: '...' });
There are 3 options:
- {} (auto option) -> does not cache
- { cache: "no-store" } -> does not cache
- { cache: "force-cache" } -> caches
From before, we know that a route becomes dynamically rendered when Next encounters a dynamic element: cookies, headers, collection, draftmode, searchParams page prop or the dynamic fetch: { cache: "no-store" }.
So, out of our 3 options above, the dynamic fetch: { cache: "no-store" }, will make the route dynamic. The other 2 will not.
1. {} auto
fetch('url');
Omitting the cache property is called the auto option. By default, the fetch result will not be cached in the data cache. The auto option does not opt the route into dynamic rendering.
-
autooption + static route:Nextwill - atbuild time- make the fetch, render the result intoHTMLandrsc. The route is cached, the result is not cached (nodata cache).Nextserves theHTMLandrscbut never makes the fetch again. -
autooption + dynamic route:Nextwill - atrequest time- make the fetch, render the result intoHTMLandrscbut not cache the fetch result. For each request, the fetch will be made and the route will be rendered (intoHTMLandrsc).
This is as expected. The auto option does not interfere with static or dynamic rendering, they behave normally. When a route is static, it remains static, when a route is dynamic it remains dynamic. In a static route, the fetch is made only once at build time. In a dynamic route, the fetch is made for each request, at request time. The fetch result is never cached.
2. { cache: "no-store" }
Setting a fetch with no-store in Next is called a dynamic fetch. Data will never be cached, we always want fresh data. You can also use no-cache. It has the same result in Next.
A dynamic fetch is a dynamic element. This means that it turns your route into dynamic rendering: server-side at request time. So, when using no-store a route can never be static: no prerendered HTML or rsc at build time. The route is skipped and only rendered at request time. So, opting out of data cache, also opts you out of the full route cache.
This makes sense, when you always want fresh data, you cannot prerender it in advance, only "now", at request time.
3. { cache: "force-cache" }
Setting cache to force-cache means that Next will use the cache as much as possible. When using force-cache, Next will first check the data cache for a matching request. If there is none or if the match is stale, a new fetch will be made and cached.
force-cache does not force a route into dynamic rendering.
- A static route with
force-cachewill remain static. Atbuild timeNextuses the cache or makes a new fetch and caches it. It then prerenders theHTMLandrsc. - A dynamic route with
force-cachewill remain dynamic. Atrequest timeNextuses the cache or makes a new fetch and caches it. It will renderHTMLandrscbut it won't cache it.
Recap
-
autoandno-storedo not cache fetch results, whereasforce-cachedoes. -
no-storeforces a route into dynamic rendering.autoandforce-cachedo not.
Testing
Let's test this out and see how it looks. Note: the examples are available on github. We created a <Post > component.
// components/Post.tsx
type Props = {
postId: number;
fetchOptions: RequestInit;
};
export type PostT = {
id: number;
title: string;
};
export default async function Post({ postId, fetchOptions }: Props) {
const data = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`,
fetchOptions,
);
const post: PostT = await data.json();
// get date from headers
const fetchDate = data.headers.get('date');
const time = fetchDate
? new Date(fetchDate).toTimeString().slice(0, 8)
: null;
return (
<div className='flex gap-2'>
<div className='text-orange-400 mr-2'>{time}</div>
<div className='font-bold'>{post.id}.</div>
<div className='italic'>{post.title}</div>
</div>
);
}
This component:
- Takes an id and fetchOptions (
{ cache: ... }) as arguments - Makes a fetch to JSONPlaceholder posts.
- Renders the date, id and title.
Note that we get the date from fetch response headers. This is the date the fetch was made. We'll be using this later on in the chapters on revalidation.
We create a static route and load a <Post /> with auto option and one with "force cache".
// app/cache/static/page.tsx
import Post from '@/components/Post';
export default function Page() {
return (
<div>
<h2 className='font-bold mb-4'>Static route + cache options</h2>
<Post postId={10} fetchOptions={{}} />
<Post postId={11} fetchOptions={{ cache: 'force-cache' }} />
</div>
);
}
We create another route:
// app/cache/notstatic/page.tsx
import Post from '@/components/Post';
export default function Page() {
return (
<div>
<h2 className='font-bold mb-4'>
Dynamic element in static route + cache options
</h2>
<Post postId={12} fetchOptions={{ cache: 'no-store' }} />
</div>
);
}
Did you catch it? A fetch with "no-store" is a dynamic fetch, forcing the route into dynamic rendering. We also want to test static rendering so we had to put the dynamic fetch (no-store) in a separate route.
Finally, a dynamic route:
// app/cache/dynamic/page.tsx
import Post from '@/components/Post';
export const dynamic = 'force-dynamic';
export default function Page() {
return (
<div>
<h2 className='font-bold mb-4'>Dynamic route + cache options</h2>
<Post postId={13} fetchOptions={{}} />
<Post postId={14} fetchOptions={{ cache: 'no-store' }} />
<Post postId={15} fetchOptions={{ cache: 'force-cache' }} />
</div>
);
}
Again, the dynamic fetch will force the route into dynamic rendering so the route segment option dynamic is not necessary but we just wanted to make it explicit. It's fine.
For a better overview, here's a schematic of the routes we build:
/static route
- fetch 10: auto
- fetch 11: force-cache
/notstatic route
- fetch 12: no-store
/dynamic route
- fetch 13: auto
- fetch 14: no-store
- fetch 15: force-cache
So, what do we expect here when we run next build?
- The static route should have been statically rendered into
HTMLandrscand be in thefull route cache. The notstatic and dynamic routes should not. - Fetch with auto cache should not be in
data cache: posts 10 and 13 should not be in cache. - Fetch with "no-store" should not be in
data cache: posts 12 and 14 should not be in cache. - Fetch with "force-cache" should be in
data cache: posts 11 and 15 should be in cache.
Two notes here:
- We're using different post ids here so other routes don't pollute the results.
- Sometimes, depending on the environment,
data cachepersists between builds. To avoid this, I delete the.nextfolder before each build.
next build
The build log confirms the route expectations:
├ ƒ /cache/dynamic
├ ƒ /cache/notstatic
├ ○ /cache/static
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
Also, In the .next/server/app/cache folder: static.html and static.rsc exist. The other routes are absent. What we confirmed here is that dynamic fetches do force routes into dynamic rendering. Great, on to the data cache.
Generated data cache files
A quick word about the actual data cache files. These are on the server (data cache is always on the server) in the build folder: .next/cache/fetch-cache. Each cached fetch has it's own file (hash name with no extension).
This is how the data cache entry for post 11 ("force-cache") looks like:
I made some markings, it contains:
- Entire response object of the fetch: headers, status, url, and the serialized body.
- Metadata from Next.js: revalidate and tags.
Don't worry about this, you don't have to know or understand this. Just remember, the entire response is saved and some Next meta data are saved. We can't read the actual data because they are serialized.
Note: I was also wondering how Next finds a match inside this cache but it's complicated and in the end it doesn't matter. Next knows how to do this and how to check if the data isn't stale. I'm leaving it at that.
Testing continued
We confirmed that static and dynamic rendering happened as expected. Now, we need to check the data cache.
/static route
- fetch 10: auto
- fetch 11: force-cache
/notstatic route
- fetch 12: no-store
/dynamic route
- fetch 13: auto
- fetch 14: no-store
- fetch 15: force-cache
We found posts 10 and 11 in the data cache, nothing else. This is unexpected. What happened?
- The "no-store" fetches behaved as expected. 12 and 14 are not cached.
- We did not expect the auto fetches (10 and 13) to be cached. But 10 was. Weird.
- We expected the "force-cache" fetches 11 and 15 to have been cached. Only 11 was. 15 is missing.
We tackle this last one first. 15 is missing. Why? Because we didn't think this through. We only ran next build. But we have a dynamic route. Dynamic routes don't get rendered at build time, only at request time. We haven't made a request for the dynamic route so fetches 13-15 haven't been made yet.
next start
We visit our 3 test routes: /cache/static, cache/notstatic and /cache/dynamic and revisit the data cache folder. One new file was added: post 15 for the "force-cache" fetch in the dynamic route. This is as expected. We expect fetch 15 with "force-cache" to have been cached. Great, first problem fixed.
The "Auto" Mystery
The second problem. Fetch 10 with auto caching should not have been cached but it was. Why? Note, we ran the dynamic route. This route also has an auto fetch (13). However, this auto fetch was not added to the cache. So 13 behaved normally and this seems to be a problem with static rendering.
I couldn't figure it out, so I asked AI. This is how Gemini explained it:
In Next.js, during the build process (Static Site Generation), fetch requests are actually cached by default even if you use the auto setting. This is because Next.js needs to ensure that the static HTML it creates has data to show. If it didn't cache that "auto" fetch during the build, the static page would have nothing to display! This is why post 10 appeared in your folder, while post 13 (the dynamic one) did not.
Seems plausible but I can't verify this is correct. This sorta insinuates that during a render, fetches are made first and then the actual render is made. Maybe.
In any case, it doesn't really matter that much. In static rendering, the API is hit once and then the route is rendered into HTML and rsc. Only this full route cache is served. The endpoint isn't hit again (unless you revalidate). We will leave it at this.
Summary
We learned how to use data cache in Next using the cache option on the fetch API. We also saw how using a dynamic fetch (no-store) forces the entire route into dynamic rendering and took a quick peek at the data cache files. Lastly we ran some tests that - mostly - confirmed the theory.
This should give you a more grounded feel for the data cache mechanism in Next. In the following chapter, we look into revalidation.
If you want to support my writing, you can donate with paypal.

Top comments (0)