Skip to content
Important
Security Advisory: React2Shell & two new vulnerabilities
Find out more
API ReferenceDirectivesuse cache: remote

use cache: remote

Last updated November 11, 2025

While the use cache directive is sufficient for most application needs, you might occasionally notice that cached operations are re-running more often than expected, or that your upstream services (CMS, databases, external APIs) are getting more hits than you'd expect. This can happen because in-memory caching has inherent limitations:

  • Cache entries being evicted to make room for new ones
  • Memory constraints in your deployment environment
  • Cache not persisting across requests or server restarts

Note that use cache still provides value beyond server-side caching: it informs Next.js what can be prefetched and defines stale times for client-side navigation.

The 'use cache: remote' directive lets you declaratively specify that a cached output should be stored in a remote cache instead of in-memory. While this gives you more durable caching for specific operations, it comes with tradeoffs: infrastructure cost and network latency during cache lookups.

Usage

To use 'use cache: remote', enable the cacheComponents flag in your next.config.ts file:

next.config.ts
import type { NextConfig } from 'next'   const nextConfig: NextConfig = {  cacheComponents: true, }   export default nextConfig

Then add 'use cache: remote' to the functions or components where you've determined remote caching is justified. The handler implementation is configured via cacheHandlers, though hosting providers should typically provide this automatically. If you're self-hosting, see the cacheHandlers configuration reference to set up your cache storage.

When to avoid remote caching

  • If you already have a server-side cache key-value store wrapping your data layer, use cache may be sufficient to include data in the static shell without adding another caching layer
  • If operations are already fast (< 50ms) due to proximity or local access, the remote cache lookup might not improve performance
  • If cache keys have mostly unique values per request (search filters, price ranges, user-specific parameters), cache utilization will be near-zero
  • If data changes frequently (seconds to minutes), cache hits will quickly go stale, leading to frequent misses and waiting for upstream revalidation

When remote caching makes sense

Remote caching provides the most value when content is deferred to request time (outside the static shell). This typically happens when a component accesses request values like cookies(), headers(), or searchParams, placing it inside a Suspense boundary. In this context:

  • Each request executes the component and looks up the cache
  • In serverless environments, each instance has its own ephemeral memory with low cache hit rates
  • Remote caching provides a shared cache across all instances, improving hit rates and reducing backend load

Compelling scenarios for 'use cache: remote':

  • Rate-limited APIs: Your upstream service has rate limits or request quotas that you risk hitting
  • Protecting slow backends: Your database or API becomes a bottleneck under high traffic
  • Expensive operations: Database queries or computations that are costly to run repeatedly
  • Flaky or unreliable services: External services that occasionally fail or have availability issues

In these cases, the cost and latency of remote caching is justified by avoiding worse outcomes (rate limit errors, backend overload, high compute bills, or degraded user experience).

For static shell content, use cache is usually sufficient. However, if your static pages share data from an upstream that can't handle concurrent revalidation requests (like a rate-limited CMS), use cache: remote acts as a shared cache layer in front of that upstream. This is the same pattern as putting a key-value store in front of a database, but declared in your code rather than configured in infrastructure.

How use cache: remote differs from use cache and use cache: private

Next.js provides three caching directives, each designed for different use cases:

Featureuse cache'use cache: remote''use cache: private'
Server-side cachingIn-memory or cache handlerRemote cache handlerNone
Cache scopeShared across all usersShared across all usersPer-client (browser)
Can access cookies/headers directlyNo (must pass as arguments)No (must pass as arguments)Yes
Server cache utilizationMay be low outside static shellHigh (shared across instances)N/A
Additional costsNoneInfrastructure (storage, network)None
Latency impactNoneCache handler lookupNone

Caching with runtime data

Both use cache and 'use cache: remote' directives can't access runtime data like cookies or search params directly, since this data isn't available when computing the cache. However, you can extract values from these APIs and pass them as arguments to cached functions. See with runtime data for this pattern.

In general, but most importantly for 'use cache: remote', be thoughtful about which values you include in cache keys. Each unique value creates a separate cache entry, reducing cache utilization. Consider this example with search filters:

app/products/[category]/page.tsx
import { Suspense } from 'react'   export default async function ProductsPage({  params,  searchParams, }: {  params: Promise<{ category: string }>  searchParams: Promise<{ minPrice?: string }> }) {  return (  <Suspense fallback={<div>Loading...</div>}>  <ProductList params={params} searchParams={searchParams} />  </Suspense>  ) }   async function ProductList({  params,  searchParams, }: {  params: Promise<{ category: string }>  searchParams: Promise<{ minPrice?: string }> }) {  const { category } = await params    const { minPrice } = await searchParams    // Cache only on category (few unique values)  // Don't include price filter (many unique values)  const products = await getProductsByCategory(category)    // Filter price in memory instead of creating cache entries  // for every price value  const filtered = minPrice  ? products.filter((p) => p.price >= parseFloat(minPrice))  : products    return <div>{/* render filtered products */}</div> }   async function getProductsByCategory(category: string) {  'use cache: remote'  // Only category is part of the cache key  // Much better utilization than caching every price filter value  return db.products.findByCategory(category) }

In this example, the remote handler stores more data per cache entry (all products in a category) to achieve better cache hit rates. This is worth it when the cost of cache misses (hitting your backend) outweighs the storage cost of larger entries.

The same principle applies to user-specific data. Rather than caching per-user data directly, use user preferences to determine what shared data to cache.

For example, if users have a language preference in their session, extract that preference and use it to cache shared content:

  • Instead of remote caching getUserProfile(sessionID), which creates one entry per user
  • Remote cache getCMSContent(language) to create one entry per language
app/components/welcome-message.tsx
import { cookies } from 'next/headers' import { cacheLife } from 'next/cache'   export async function WelcomeMessage() {  // Extract the language preference (not unique per user)  const language = (await cookies()).get('language')?.value || 'en'    // Cache based on language (few unique values: en, es, fr, de, etc.)  // All users who prefer 'en' share the same cache entry  const content = await getCMSContent(language)    return <div>{content.welcomeMessage}</div> }   async function getCMSContent(language: string) {  'use cache: remote'  cacheLife({ expire: 3600 })  // Creates ~10-50 cache entries (one per language)  // instead of thousands (one per user)  return cms.getHomeContent(language) }

This way all users who prefer the same language share a cache entry, improving cache utilization and reducing load on your CMS.

The pattern is the same in both examples: find the dimension with fewer unique values (category vs. price, language vs. user ID), cache on that dimension, and filter or select the rest in memory.

If the service used by getUserProfile cannot scale with your frontend load, you may still be able to use the use cache directive with a short cacheLife for in-memory caching. However, for most user data, you likely want to fetch directly from the source (which might already be wrapped in a key/value store as mentioned in the guidelines above).

Only use 'use cache: private' if you have compliance requirements or can't refactor to pass runtime data as arguments.

Nesting rules

Remote caches have specific nesting rules:

  • Remote caches can be nested inside other remote caches ('use cache: remote')
  • Remote caches can be nested inside regular caches ('use cache')
  • Remote caches cannot be nested inside private caches ('use cache: private')
  • Private caches cannot be nested inside remote caches
// VALID: Remote inside remote async function outerRemote() {  'use cache: remote'  const result = await innerRemote()  return result }   async function innerRemote() {  'use cache: remote'  return getData() }   // VALID: Remote inside regular cache async function outerCache() {  'use cache'  // The inner remote cache will work when deferred to request time  const result = await innerRemote()  return result }   async function innerRemote() {  'use cache: remote'  return getData() }   // INVALID: Remote inside private async function outerPrivate() {  'use cache: private'  const result = await innerRemote() // Error!  return result }   async function innerRemote() {  'use cache: remote'  return getData() }   // INVALID: Private inside remote async function outerRemote() {  'use cache: remote'  const result = await innerPrivate() // Error!  return result }   async function innerPrivate() {  'use cache: private'  return getData() }

Examples

The following examples demonstrate common patterns for using 'use cache: remote'. For details about cacheLife parameters (stale, revalidate, expire), see the cacheLife API reference.

With user preferences

Cache product pricing based on the user's currency preference. Since the currency is stored in a cookie, this component renders at request time. Remote caching is valuable here because all users with the same currency share the cached price, and in serverless environments, all instances share the same remote cache.

app/product/[id]/page.tsx
import { Suspense } from 'react' import { cookies } from 'next/headers' import { cacheTag, cacheLife } from 'next/cache'   export async function generateStaticParams() {  return [{ id: '1' }, { id: '2' }, { id: '3' }] }   export default async function ProductPage({  params, }: {  params: Promise<{ id: string }> }) {  const { id } = await params    return (  <div>  <ProductDetails id={id} />  <Suspense fallback={<div>Loading price...</div>}>  <ProductPrice productId={id} />  </Suspense>  </div>  ) }   function ProductDetails({ id }: { id: string }) {  return <div>Product: {id}</div> }   async function ProductPrice({ productId }: { productId: string }) {  // Reading cookies defers this component to request time  const currency = (await cookies()).get('currency')?.value ?? 'USD'    // Cache the price per product and currency combination  // All users with the same currency share this cache entry  const price = await getProductPrice(productId, currency)    return (  <div>  Price: {price} {currency}  </div>  ) }   async function getProductPrice(productId: string, currency: string) {  'use cache: remote'  cacheTag(`product-price-${productId}`)  cacheLife({ expire: 3600 }) // 1 hour    // Cached per (productId, currency) - few currencies means high cache utilization  return db.products.getPrice(productId, currency) }

Reducing database load

Cache expensive database queries, reducing load on your database. In this example, we don't access cookies(), headers(), or searchParams. If we had a requirement to not include these stats in the static shell, we could use connection() to explicitly defer to request time:

app/dashboard/page.tsx
import { Suspense } from 'react' import { connection } from 'next/server' import { cacheLife, cacheTag } from 'next/cache'   export default function DashboardPage() {  return (  <Suspense fallback={<div>Loading stats...</div>}>  <DashboardStats />  </Suspense>  ) }   async function DashboardStats() {  // Defer to request time  await connection()    const stats = await getGlobalStats()    return <StatsDisplay stats={stats} /> }   async function getGlobalStats() {  'use cache: remote'  cacheTag('global-stats')  cacheLife({ expire: 60 }) // 1 minute    // This expensive database query is cached and shared across all users,  // reducing load on your database  const stats = await db.analytics.aggregate({  total_users: 'count',  active_sessions: 'count',  revenue: 'sum',  })    return stats }

With this setup, your upstream database sees at most one request per minute, regardless of how many users visit the dashboard.

API responses in streaming contexts

Cache API responses that are fetched during streaming or after dynamic operations:

app/feed/page.tsx
import { Suspense } from 'react' import { connection } from 'next/server' import { cacheLife, cacheTag } from 'next/cache'   export default async function FeedPage() {  return (  <div>  <Suspense fallback={<Skeleton />}>  <FeedItems />  </Suspense>  </div>  ) }   async function FeedItems() {  // Defer to request time  await connection()    const items = await getFeedItems()    return items.map((item) => <FeedItem key={item.id} item={item} />) }   async function getFeedItems() {  'use cache: remote'  cacheTag('feed-items')  cacheLife({ expire: 120 }) // 2 minutes    // This API call is cached, reducing requests to your external service  const response = await fetch('https://api.example.com/feed')  return response.json() }

Computed data after dynamic checks

Cache expensive computations that occur after dynamic security or feature checks:

app/reports/page.tsx
import { connection } from 'next/server' import { cacheLife } from 'next/cache'   export default async function ReportsPage() {  // Defer to request time (for security check)  await connection()    const report = await generateReport()    return <ReportViewer report={report} /> }   async function generateReport() {  'use cache: remote'  cacheLife({ expire: 3600 }) // 1 hour    // This expensive computation is cached and shared across all authorized users,  // avoiding repeated calculations  const data = await db.transactions.findMany()    return {  totalRevenue: calculateRevenue(data),  topProducts: analyzeProducts(data),  trends: calculateTrends(data),  } }

Mixed caching strategies

Combine static, remote, and private caching for optimal performance:

app/product/[id]/page.tsx
import { Suspense } from 'react' import { connection } from 'next/server' import { cookies } from 'next/headers' import { cacheLife, cacheTag } from 'next/cache'   // Static product data - prerendered at build time async function getProduct(id: string) {  'use cache'  cacheTag(`product-${id}`)    // This is cached at build time and shared across all users  return db.products.find({ where: { id } }) }   // Shared pricing data - cached at runtime in remote handler async function getProductPrice(id: string) {  'use cache: remote'  cacheTag(`product-price-${id}`)  cacheLife({ expire: 300 }) // 5 minutes    // This is cached at runtime and shared across all users  return db.products.getPrice({ where: { id } }) }   // User-specific recommendations - private cache per user async function getRecommendations(productId: string) {  'use cache: private'  cacheLife({ expire: 60 }) // 1 minute    const sessionId = (await cookies()).get('session-id')?.value    // This is cached per-user and never shared  return db.recommendations.findMany({  where: { productId, sessionId },  }) }   export default async function ProductPage({ params }) {  const { id } = await params    // Static product data  const product = await getProduct(id)    return (  <div>  <ProductDetails product={product} />    {/* Dynamic shared price */}  <Suspense fallback={<PriceSkeleton />}>  <ProductPriceComponent productId={id} />  </Suspense>    {/* Dynamic personalized recommendations */}  <Suspense fallback={<RecommendationsSkeleton />}>  <ProductRecommendations productId={id} />  </Suspense>  </div>  ) }   function ProductDetails({ product }) {  return (  <div>  <h1>{product.name}</h1>  <p>{product.description}</p>  </div>  ) }   async function ProductPriceComponent({ productId }) {  // Defer to request time  await connection()    const price = await getProductPrice(productId)  return <div>Price: ${price}</div> }   async function ProductRecommendations({ productId }) {  const recommendations = await getRecommendations(productId)  return <RecommendationsList items={recommendations} /> }   function PriceSkeleton() {  return <div>Loading price...</div> }   function RecommendationsSkeleton() {  return <div>Loading recommendations...</div> }   function RecommendationsList({ items }) {  return (  <ul>  {items.map((item) => (  <li key={item.id}>{item.name}</li>  ))}  </ul>  ) }

Good to know:

  • Remote caches are stored in server-side cache handlers and shared across all users
  • 'use cache: remote' works outside the static shell where use cache may not provide server-side cache hits
  • Use cacheTag() and revalidateTag() to invalidate remote caches on-demand
  • Use cacheLife() to configure cache expiration
  • For user-specific data, use 'use cache: private' instead of 'use cache: remote'
  • Remote caches reduce origin load by storing computed or fetched data server-side

Platform Support

Deployment OptionSupported
Node.js serverYes
Docker containerYes
Static exportNo
AdaptersYes

Version History

VersionChanges
v16.0.0"use cache: remote" is enabled with the Cache Components feature.

Was this helpful?