Type something to search...
Next.js use cache: remote

Next.js use cache: remote

If you've adopted Cache Components and started sprinkling 'use cache' through your app, you'll eventually hit a wall: some of your cached functions just don't behave the way you'd expect. A function you cached to protect a rate-limited API still seems to hit that API constantly. A dashboard query that should only run once a minute somehow fires on every single request. You didn't do anything wrong — you just ran into the fundamental limitation of 'use cache': it's an in-memory cache, and in-memory caches only help when the memory in question is actually shared.

'use cache: remote' exists to fix exactly that gap. It's the directive you reach for when a cached function needs to be deferred to request time — meaning it lives outside the static shell, inside a Suspense boundary, next to a cookies() or headers() call — and you still want real cache hits across every server instance handling your traffic. This article walks through what problem it solves, when it's worth the tradeoff, and the patterns Next.js expects you to follow when you use it.

Why 'use cache' alone isn't enough

To understand why 'use cache: remote' needs to exist at all, it helps to be precise about what 'use cache' actually does. When you mark a function or component with 'use cache', Next.js stores its result in memory on the server process that ran it. That's genuinely useful for two reasons even before you think about performance: it tells Next.js what content can be prefetched ahead of navigation, and it defines how long that content should be considered "fresh" for client-side transitions. Those benefits show up regardless of whether the cache ever gets a hit.

But the storage mechanism itself — plain server memory — has real limits:

  • Entries get evicted to make room for newer ones once memory pressure builds up.
  • The deployment environment itself may impose tight memory constraints.
  • None of it persists across a request boundary in serverless environments, and none of it survives a server restart.

That last point is the one that bites people the hardest. If you're deploying to a serverless platform — which is the default assumption for most Next.js hosting today — every invocation can spin up on a fresh instance with its own private memory. A cached function inside a Suspense boundary, executing at request time, might get a completely cold cache on every single request simply because the instance that served the previous request isn't the instance serving this one. You added 'use cache', watched your monitoring dashboard, and the hit rate never moved.

'use cache: remote' is the fix: instead of storing the result in that ephemeral, per-instance memory, it stores it in a remote cache handler — something durable and shared, sitting outside any single server process, that every instance can read from and write to. The tradeoff is exactly what you'd expect from moving storage off-box: you're now paying for infrastructure (the remote store itself) and for network latency on every cache lookup. It's not a free upgrade. It's a deliberate trade of "always instant, sometimes wrong" for "usually instant after a network round trip, and actually shared."

Turning it on

Before you can use 'use cache: remote' anywhere, you need Cache Components enabled via the cacheComponents flag in next.config.ts (or .js):

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;
/** @type {import('next').NextConfig} */
const nextConfig = {
  cacheComponents: true,
};

module.exports = nextConfig;

With that flag on, you add 'use cache: remote' as the first line inside any async function or component where you've decided remote caching is actually justified — more on that decision below. The remote store itself isn't something you configure inline; it's wired up through cacheHandlers in your Next.js config. If you're on a managed hosting provider, this is very often already set up for you out of the box — that's part of what "the platform supports Cache Components" is meant to imply. If you're self-hosting, you're responsible for pointing cacheHandlers at whatever storage backend you're running (Redis, a managed KV store, or anything else that fits the cache handler interface), which is worth reading the cacheHandlers configuration reference for before you commit to a backend.

Deciding whether you actually need it

This is the part of the directive that's easy to get wrong in both directions — either avoiding it because it sounds heavyweight, or reaching for it reflexively because "shared cache" sounds strictly better than "local cache." Neither instinct is reliable. Here's a more useful way to think about it.

Skip remote caching when:

  • You already have a key-value store wrapping your data layer. In that case 'use cache' is enough to get the data into the static shell; you don't need a second caching layer duplicating what your data layer already does.
  • The operation is already fast — under roughly 50ms — because it's local or geographically close. A network round trip to a remote cache can easily cost more than just re-running the fast operation.
  • Your cache keys are mostly unique per request. If every request produces a different key (arbitrary search filters, exact price ranges, per-user parameters), your cache utilization approaches zero no matter where you store the entries — you're paying for infrastructure that never gets reused.
  • The underlying data changes every few seconds to minutes. Entries go stale almost immediately, so you're mostly paying network latency for cache misses followed by real work anyway.

Reach for it when:

  • Content is genuinely deferred to request time — it reads cookies(), headers(), or searchParams, which places it inside a Suspense boundary rather than the static shell. This is the precondition that makes remote caching meaningful at all, because it's exactly the scenario where per-instance memory caching fails hardest in serverless environments.
  • You're protecting a rate-limited or quota-constrained upstream — a third-party API you risk exceeding.
  • A slow database or backend becomes a bottleneck under real traffic, and you'd rather absorb load at the cache layer than at the origin.
  • The work itself is expensive — a heavy aggregation query, a costly computation — and re-running it per-instance, per-request is wasteful even before you think about upstream load.
  • You're calling a flaky or occasionally unavailable service, and a shared cache gives you a fallback layer that reduces how often you're exposed to that flakiness.

The mental model that ties these together: 'use cache: remote' earns its cost when the alternative — hitting the real backend on every request from every instance — is worse than paying for a network hop to a shared store. If the backend genuinely doesn't mind the load, you're just adding latency for no benefit.

How it compares to the other two caching directives

Next.js gives you three caching directives that look similar syntactically but solve different problems. It's worth seeing them side by side, because reaching for the wrong one is an easy mistake once you're moving fast:

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
Persists across deploysNoNoN/A

The row worth internalizing is "can access cookies/headers directly." Both use cache and 'use cache: remote' are shared-scope caches — the whole point is that many users hit the same entry — so neither one is allowed to reach into request-specific data directly. If you need the currency, locale, or session tied to a specific visitor, you extract that value outside the cached function and pass it in as an argument. 'use cache: private' is the odd one out: it's scoped per browser, so it's allowed to read cookies() directly, but it never gets shared across users, which makes it the wrong tool for the load-reduction goals remote caching is built for.

Persistence across deploys — and why that's a feature, not a bug

One property of 'use cache: remote' surprises people the first time they notice it: cache entries do not carry over from one deployment to the next. The cache key includes your deploymentId (if configured) or your build ID, so a fresh build produces fresh keys, and every entry from the previous build becomes permanently unreachable.

This is deliberate, not an oversight. Between deploys, the underlying shape of a cached function's return value — or its identity — can change in ways that matter: a CMS client library gets upgraded, a cached function gets refactored, a dependency changes its serialization format. If old cache entries survived into the new build, you'd risk a caller in the new code silently receiving a value shaped for the old code. Starting from a clean cache on every deploy trades a burst of cold-cache misses right after launch for the guarantee that nothing stale or malformed leaks across a release boundary.

If you genuinely need caching that survives deploys — a very different requirement from what remote caching is solving — that's what unstable_cache (for non-fetch functions) or the fetch cache itself are for. Don't try to bend 'use cache: remote' into that shape; it's the wrong tool for it by design.

The part nobody skips by accident: cache key discipline

This is, in practice, where most of the value of 'use cache: remote' is won or lost, and it's the section of the docs worth reading twice. Every distinct set of argument values you pass into a 'use cache: remote' function produces a separate cache entry. If your arguments have high cardinality, your "shared" cache quietly turns into thousands of single-use entries, and you get all of the infrastructure cost with none of the hit-rate benefit.

Take a product listing page with a category and a price filter:

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);
}

Notice what's not passed into the cached function: minPrice. That value can take on essentially unlimited unique values, so including it in the cache key would mean nearly every request creates its own single-use entry. Instead, the cached function stores the full category result set — a larger payload per entry, but far fewer entries overall — and the price filter is applied afterward, in memory, on the already-fetched data. Trading entry size for entry count is very often the right call when the alternative is a cache that never gets reused.

The same principle applies to anything that looks user-specific at first glance but is really a proxy for something with much lower cardinality. A session ID has essentially unlimited unique values; a language preference derived from that session doesn't:

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);
}

Caching getUserProfile(sessionID) directly would create one entry per user — effectively no sharing at all. Caching getCMSContent(language) creates a handful of entries, one per supported language, and every user who shares a language preference shares the cache hit. The dimension you cache on matters far more than whether you cache at all.

If a service genuinely can't be refactored to accept a low-cardinality argument like this, and it truly needs per-user caching, that's what 'use cache: private' exists for — but reach for it as the exception, not the default, since it never gives you the cross-instance sharing that's the entire point of going remote in the first place.

Nesting rules you need to know before you hit them

Because these three directives can wrap each other, Next.js enforces a small set of nesting rules to avoid combinations that don't make sense:

  • Remote caches can nest inside other remote caches.
  • Remote caches can nest inside regular ('use cache') caches.
  • Remote caches cannot nest inside private caches.
  • Private caches cannot nest 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";
  const result = await innerRemote();
  return result;
}

// INVALID: Remote inside private — and vice versa
async function outerPrivate() {
  "use cache: private";
  const result = await innerRemote(); // Error!
  return result;
}

The logic behind the restriction is scope compatibility: private caches are inherently per-user, remote caches are inherently shared, and mixing them in either nesting direction would mean a shared cache entry accidentally capturing per-user state, or a per-user cache uselessly wrapping something meant to be shared. Next.js catches this at build/runtime rather than letting it silently produce a cache that leaks the wrong scope.

A realistic pattern: mixed caching in one page

Most real pages don't use just one of these directives — they layer all three based on what each piece of data actually needs:

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}`);
  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
  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;
  return db.recommendations.findMany({ where: { productId, sessionId } });
}

Product data that's identical for every visitor gets the cheapest option — regular 'use cache', baked into the static shell. Price data changes with currency but is still shared across everyone using that currency, so it gets 'use cache: remote' with a cacheTag for later on-demand invalidation. Recommendations are inherently per-visitor, so they get 'use cache: private', accepting that they'll never be shared but gaining direct access to session cookies. None of these decisions were arbitrary — each one follows from asking "who else, besides this one visitor, would want this exact value?"

Invalidating and expiring remote cache entries

'use cache: remote' entries work with the same on-demand invalidation and expiration tools as 'use cache'. Tag an entry with cacheTag(), and later call revalidateTag() when the underlying data changes — a webhook from your CMS, a mutation in a Server Action, whatever your actual invalidation trigger is. Set a cacheLife() policy to control how long an entry is considered fresh before Next.js attempts to revalidate it. Neither of those tools behaves differently just because the storage happens to be remote instead of in-memory; the invalidation model is consistent across both.

What's worth calling out explicitly, because it's easy to reach for the wrong lever under pressure: if you're seeing stale data from a 'use cache: remote' entry, the fix is almost always a tag-based revalidation triggered from wherever the source data actually changes — not a shorter cacheLife. Shrinking the expiration window just trades staleness for more frequent (and more expensive) cache misses; it doesn't address why the data went stale in the first place.

Platform support

Because a remote cache depends on an actual storage backend being available at runtime, not every deployment target supports it the same way:

Deployment OptionSupported
Node.js serverYes
Docker containerYes
Static exportNo
AdaptersYes

Static export is the one to flag if you're evaluating this for a project that currently ships as a static site — there's no server process to run a remote cache handler against, so the feature simply isn't applicable there. If you're on a managed platform (an adapter-based deployment), the cache handler is typically wired up for you; self-hosting on a Node.js server or in a container means you're responsible for standing up and configuring that backend yourself.

Key takeaways

QuestionAnswer
What problem does it solve?Shares cached values across server instances, unlike 'use cache', which only caches per-instance in memory
When should I use it?When content is deferred to request time (reads cookies/headers/searchParams) and the upstream is rate-limited, slow, expensive, or flaky
When should I skip it?When you already have a KV layer, operations are already fast, cache keys have high cardinality, or data changes every few seconds
What's the cost?Infrastructure for the remote store, plus network latency on every cache lookup
Does it persist across deploys?No — deliberately, to avoid stale or malformed values crossing a release boundary
How do I invalidate it?cacheTag() + revalidateTag(), same as 'use cache'
Can it access cookies/headers directly?No — extract the value and pass it as an argument, same restriction as 'use cache'
What if I need per-user caching instead?Use 'use cache: private', and treat it as the exception rather than the default

'use cache: remote' isn't a strictly-better version of 'use cache' — it's a different tool for a different failure mode. Reach for it specifically when request-time content needs to survive across instances and the upstream it protects genuinely can't take unmitigated load, and spend real effort choosing cache keys with low cardinality. Skip it everywhere else, and let plain 'use cache' do the simpler job it's already good at.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper