Avoid redundant calls using React's cache() for non-fetch operations, automatic fetch deduplication for API calls, and the experimental 'use cache' directive for advanced scenarios with persistent caching
In Next.js App Router, redundant data fetching across multiple Server Components on the same page is a common concern, especially when components like layouts, pages, and generateMetadata all need the same data [citation:1][citation:10]. Next.js provides multiple layers of deduplication: automatic fetch deduplication for HTTP requests, React's cache() function for any async operation, and the experimental 'use cache' directive for persistent caching. The key insight is that data fetching in Server Components is automatically deduplicated within a single render pass, but only when using the right tools for each scenario [citation:4][citation:9].
Multiple components (layout, page, generateMetadata) often need the same data like user profiles or blog posts [citation:1].
Without deduplication, each component makes separate API/database calls, increasing server load and response time [citation:2].
This is especially problematic with ORMs or database clients that don't automatically deduplicate like fetch() does [citation:4].
The issue affects both performance and cost, particularly with paid API services or rate-limited endpoints [citation:10].
When using the native fetch() API within Server Components, Next.js automatically deduplicates requests. React patches globalThis.fetch on the server side, so that multiple calls to the same URL with the same options within a single render pass will result in only one actual network request [citation:4]. This works because fetch responses are memoized per request, stored in the React cache for the duration of the render pass [citation:8]. This is the simplest solution and requires no additional code—just use fetch consistently across components [citation:1].
For data fetching that doesn't use fetch—such as database queries with ORMs (Prisma, Drizzle), SDKs, or custom API clients—you need to manually opt into deduplication using React's cache() function [citation:4]. cache() creates a memoized version of your async function that returns the same promise for identical arguments within a single render pass [citation:10]. This is perfect for database calls or when you need to deduplicate expensive computations across components like layouts, pages, and generateMetadata [citation:2][citation:9]. The cache is per-request only and doesn't persist across different user requests [citation:4].
React's cache() uses shallow equality checks on arguments. If you pass objects, two calls with identically structured but different object references won't be deduped [citation:10].
❌ getProduct({ id: '123' }) and getProduct({ id: '123' }) → two separate calls (different object references)
✅ getProduct('123') and getProduct('123') → single call (primitive string equality)
✅ getProduct(id) with the same variable → single call
Solution: Pass primitives (strings, numbers) rather than objects as arguments, or ensure object references are identical [citation:10].
You can combine both approaches by wrapping fetch-based operations in cache() when you need additional control or when working with fetch variants that might not be automatically deduped. Some HTTP clients like Ky don't automatically participate in React's fetch deduplication, requiring explicit cache() wrapping [citation:4]. This pattern also allows you to add logging, transformation, or error handling while still benefiting from deduplication.
While React's cache() only deduplicates within a single request, Next.js provides unstable_cache (from 'next/cache') for persisting results across multiple requests and even deployments [citation:4]. This is ideal for expensive computations or data that rarely changes. Unlike React.cache, unstable_cache writes to the filesystem or persistent data cache, meaning 100 page visits might only trigger one database query [citation:4]. Use this for truly static or slowly changing data, but be cautious with user-specific data as it's shared across users.
Next.js is introducing a new experimental 'use cache' directive that simplifies caching dramatically [citation:5]. You can add 'use cache' to any async function or component, and Next.js automatically handles deduplication, persistent caching, and invalidation. This unifies the mental model: instead of choosing between fetch, cache(), and unstable_cache, you simply mark what should be cached and let the framework decide how [citation:5][citation:3]. Combined with cacheTag() and cacheLife(), you get fine-grained control over cache behavior. This is experimental and requires the dynamicIO flag, but represents the future of caching in Next.js [citation:5].
Use automatic fetch deduplication when: You're using native fetch() for API calls and don't need custom logic [citation:4][citation:8].
Use React.cache() when: You're using database clients (Prisma, Drizzle), SDKs, or any async function that isn't fetch [citation:4][citation:9].
Use React.cache() also when: You need to dedupe across generateMetadata and page components with custom data fetching [citation:1][citation:10].
Use unstable_cache when: You need to cache across different user requests for expensive operations (site stats, global config) [citation:4].
Consider 'use cache' (experimental) for: New projects where you want the simplest mental model and can tolerate experimental APIs [citation:5].
A frequent issue is deduplication not working with generateMetadata and page components [citation:10]. This often happens because: 1) Using an HTTP client like Axios or Ky that doesn't use fetch [citation:4], 2) Passing objects as arguments to cached functions [citation:10], or 3) The functions executing in parallel before cache can establish the promise. Solution: Ensure you're using fetch or wrap all data access in cache(), and pass primitive arguments. Also note that cache() only works on the server—client components need different strategies [citation:4].