fetch() extensions
Extends the Web fetch API with server-side caching and tag-based revalidation options.
fetch(url: string | URL | Request, options?: RequestInit & { cache?: 'force-cache' | 'no-store'; next?: { revalidate?: false | 0 | number; tags?: string[] } }): Promise<Response>- Added in Next.js v13.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$url | string | URL | Request | The resource to fetch, exactly as in the Web fetch API. |
$options.cacheoptional | 'force-cache' | 'no-store'= auto no cache | How the request interacts with the Next.js persistent cache. 'force-cache' looks for a match keyed on URL, method, headers and body, and stores only 200 responses. 'no-store' fetches from the remote server on every request. The unset default is 'auto no cache': fetched once during next build because the route prerenders, and per request if the route uses a request-time API. |
$options.next.revalidateoptional | false | 0 | number | Cache lifetime in seconds. false caches indefinitely, 0 prevents caching, a number sets a lifetime of at most n seconds. |
$options.next.tagsoptional | string[] | Cache tags for this resource, so it can be revalidated on demand with revalidateTag. Max 256 characters per tag and max 128 tags. |
Return value
Promise<Response>A standard Web Response, served from the framework's persistent cache when the options and cache state allow it.
Errors
Conflicting options such as { revalidate: 3600, cache: 'no-store' } are not allowed: both are ignored, and in development a warning is printed to the terminal.
Changelog
- PHP v15.0.0Caching became opt-in: fetch responses are no longer cached by default, so code relying on the old implicit force-cache must set cache: 'force-cache' explicitly.
- PHP v13.0.0fetch introduced with the Next.js caching extensions.
Next.js replaces the global fetch on the server so each call can declare its own persistent caching. In the browser cache refers to the HTTP cache; here it refers to the framework's data cache, which survives across requests and deployments of the same build.
When to reach for each option
Leave it alone for data that can be captured at build time. Set cache: 'force-cache' to deliberately store a response, including POST requests and ones carrying authorization or cookie headers. Add next: { revalidate: n } for content that may go stale on a clock, and next: { tags: [...] } when a CMS webhook or a Server Function should invalidate it on demand with revalidateTag. Use no-store for anything per-user.
What trips people up
- Caching is opt-in since 15. Upgrades that assumed responses were cached suddenly hammer the origin.
- Two fetches to the same URL in one route with different
revalidatevalues resolve to the lower one, and a fetch'srevalidatelower than the segment's shortens the whole route. - In development the HMR cache holds even uncached responses between refreshes, so "stale" data locally is often not a caching bug.
GETrequests with identical URL and options are memoized within a single render pass — but not in Route Handlers.
Example
export default async function Page() {
const res = await fetch('https://api.vercel.app/blog', {
cache: 'force-cache',
next: { revalidate: 3600, tags: ['posts'] },
})
const posts = await res.json()
return (
<ul>
{posts.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}When to use it
- Cache a public blog index and revalidate it every hour
- Tag CMS content so a publish webhook can purge exactly the affected pages
- Force no-store on a per-user endpoint that must never be shared between visitors
- Opt a POST request into the data cache when its response is genuinely shared
Frequently asked questions
Why is Next.js no longer caching my fetch requests?
Because caching became opt-in in Next.js 15 — responses are uncached by default. Add cache: 'force-cache' to any request you want stored, optionally with next: { revalidate } to give it a lifetime.
How do I stop fetch from caching a response?
Pass cache: 'no-store', which fetches from the remote server on every request even when the route has no request-time API. Do not combine it with next: { revalidate } — conflicting options are ignored and warned about in development.
Why does my no-store fetch still show stale data in development?
The dev server keeps an HMR cache of fetch responses across hot reloads, including ones marked no-store, to cut billed API calls. It clears on navigation or a full page reload, and can be turned off with the serverComponentsHmrCache config option.
More examples
Per-user data must not be cached
no-store guarantees the response is fetched from the origin on every request even though the route already uses a request-time API. Note the contrast with force-cache, which would key the cache entry on the authorization header — correct in principle, but one wrong header normalisation upstream and you serve one user's account to another.
import { headers } from 'next/headers'
export default async function AccountPage() {
const authorization = (await headers()).get('authorization') ?? ''
const res = await fetch('https://api.example.com/me', {
headers: { authorization },
cache: 'no-store',
})
const user = await res.json()
return <h1>{user.name}</h1>
}On-demand purge with tags
Two tags on one entry give you both a broad purge ('products') and a surgical one. Tags are capped at 256 characters each and 128 per request, which is generous but does rule out tagging by long composite keys. Without force-cache there is nothing stored for the tag to invalidate.
// Fetch, tagged:
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
cache: 'force-cache',
next: { tags: ['products', `product-${id}`] },
})
return res.json()
}
// Route Handler your CMS calls on publish:
import { revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const { id } = await request.json()
revalidateTag(`product-${id}`)
return Response.json({ revalidated: true })
}Memoization inside one render pass
Identical GET requests are memoized for the duration of a single server render, so a layout and a page can both call getSettings() and the origin sees one request. This is separate from the persistent cache: memoization lasts one render pass, force-cache lasts across requests. Route Handlers are outside the React tree and get no memoization.
async function getSettings() {
const res = await fetch('https://api.example.com/settings', {
cache: 'force-cache',
})
return res.json()
}
export default async function Layout({
children,
}: {
children: React.ReactNode
}) {
const settings = await getSettings() // executed
return (
<html lang={settings.locale}>
<body>{children}</body>
</html>
)
}
// app/page.tsx calls getSettings() too — deduplicated, not re-executed
Discussion