DEV Community

Cover image for Next.js Server Component fetch cache: no-store: the performance trap
Victor Caña
Victor Caña

Posted on

Next.js Server Component fetch cache: no-store: the performance trap

You added cache: 'no-store' to a fetch call inside a Server Component because you needed fresh data. Reasonable. Standard. The docs even show it. What they don't tell you is that in the wrong place, that single option can turn one page load into hundreds of server invocations.


The context

Server Components in Next.js 14 look deceptively simple. They run on the server, they fetch data, they render HTML. No client bundle, no useEffect, no loading states. Clean.

But Server Components aren't rendered once and cached like a static page. Depending on where they sit in your component tree and how your routes are configured, they can re-execute on every request, or worse, on every rerender triggered by a parent.

Add cache: 'no-store' to a fetch inside one of these components, and you've just told Next.js: skip every layer of caching, always go to the origin, on every single execution.


The problematic code

This is the pattern that causes the issue:

// app/dashboard/page.tsx: a dynamic route
async function getMarketData(query: string) {
  const res = await fetch(`https://your-api.com/analyze?q=${query}`, {
    cache: 'no-store', // 👈 "I need fresh data"
  })
  return res.json()
}

export default async function DashboardPage({ searchParams }) {
  const data = await getMarketData(searchParams.query)
  return <ResultsView data={data} />
}
Enter fullscreen mode Exit fullscreen mode

Looks fine. But now imagine this page is wrapped in a layout that re-renders on navigation, or the component is used inside a Suspense boundary that retries on error, or you're running in a deployment where Vercel's infrastructure invokes the function per request segment.

Each execution → one fetch call → one external API hit. At scale, or with any retry/rerender loop, this compounds fast.

In my case with ReadyToRelease, a single user session generated 776,000 Vercel function invocations in under 24 hours. The root cause was exactly this pattern, cache: 'no-store' inside a Server Component on a dynamic route with no deduplication in place.


Why cache: 'no-store' is dangerous here

Next.js has its own fetch deduplication layer. When you call the same URL multiple times during a single render pass, Next.js deduplicates those requests automatically, but only when using the default cache behavior or cache: 'force-cache'.

The moment you use cache: 'no-store', you opt out of that deduplication. Every call is treated as unique. Every render = a real network request.

// This is deduplicated across the render tree ✅
const res = await fetch('https://api.example.com/data')

// This is NOT deduplicated: each call hits the network ❌
const res = await fetch('https://api.example.com/data', {
  cache: 'no-store'
})
Enter fullscreen mode Exit fullscreen mode

If your component tree renders the same Server Component multiple times (parallel routes, multiple Suspense boundaries, layout + page both fetching), you're making multiple real requests.


What to use instead

Option 1: next: { revalidate: N }: time-based freshness

const res = await fetch('https://your-api.com/analyze', {
  next: { revalidate: 60 } // Fresh every 60 seconds, cached in between
})
Enter fullscreen mode Exit fullscreen mode

You get reasonably fresh data without hammering the origin on every render. Good for data that changes, but not by the millisecond.

Option 2: Route Segment Config: control at the route level

// app/dashboard/page.tsx
export const dynamic = 'force-dynamic' // entire route opts out of static
export const fetchCache = 'default-no-store' // fetch behavior for this segment

// Then your fetch stays clean:
const res = await fetch('https://your-api.com/analyze')
Enter fullscreen mode Exit fullscreen mode

This separates the concern. The route is dynamic, but fetch deduplication still applies within a single render pass.

Option 3: Move the fetch outside the component tree

// lib/market-data.ts
import { cache } from 'react'

export const getMarketData = cache(async (query: string) => {
  const res = await fetch(`https://your-api.com/analyze?q=${query}`)
  return res.json()
})
Enter fullscreen mode Exit fullscreen mode

React's cache() function memoizes the result per request. Call it from ten different components in the same render, it executes once. This is the correct primitive for deduplication in Server Components.


The general rule

cache: 'no-store' doesn't mean "always fresh per user session." It means "always fresh per execution." In a Server Component, those are not the same thing.

Use cache: 'no-store' only when you understand exactly how many times that component will execute per request,
and that number is one. Otherwise, use revalidate, route-level config, or React.cache().


Conclusion

The fetch API in Next.js Server Components looks like the standard browser fetch. It isn't. The caching layer underneath has different semantics, and cache: 'no-store' removes a safety net you probably didn't know was there. Learn where your deduplication comes from before you opt out of it.

Top comments (0)