Architecture Overview
Server Runtime
Rendering Pipeline
Client Navigation
Caching and Export
Development Tools
Build and Configuration
Ecosystem Packages
Testing Infrastructure
How It Works
The following files were used as context for generating this wiki page:
Function caching provides robust execution wrapping, key encoding, and response streaming capabilities for 'use cache' operations within the App Router architecture. It addresses challenges related to concurrent function re-executions, storage isolation, and request context preservation across both Node.js server and Edge runtime environments. Key design decisions involve leveraging LRU-based memory stores, asynchronous worker pools for state exploration, and interoperability layers with legacy caching mechanisms like unstable_cache.
Sources: packages/next/src/server/use-cache/use-cache-wrapper.ts, packages/next/src/server/web/spec-extension/unstable-cache.ts, packages/next/src/server/lib/cache-handlers/default.ts, packages/next/src/server/dev/use-cache-probe-pool.ts, packages/next/src/server/web/edge-route-module-wrapper.ts
Function caching wraps 'use cache' invocations to enforce execution constraints, route lookup requests through registered cache handlers, and manage React flight stream caching. When a cached function is invoked, the wrapper validates the active work store context, resolves the appropriate storage backend, and coordinates retrieval or re-execution.
Sources: packages/next/src/server/use-cache/use-cache-wrapper.ts, packages/next/src/server/use-cache/handlers.ts
The core retrieval and execution path flows through specific module boundaries during a cache lookup operation.
cache (packages/next/src/server/use-cache/use-cache-wrapper.ts): Intercepts the function call, validates the workAsyncStorage and workUnitStore contexts, determines whether the function is private or public, constructs cache contexts, and invokes handler resolution.
Sources: packages/next/src/server/use-cache/use-cache-wrapper.tsgetCacheHandler (packages/next/src/server/use-cache/handlers.ts): Queries the global handlersMapSymbol map using the specified cache kind (e.g., 'default' or 'remote'), throwing an error if cache handlers have not been initialized or returning the requested CacheHandler instance.
Sources: packages/next/src/server/use-cache/handlers.tsget (packages/next/src/server/lib/cache-handlers/default.ts): Executes on the resolved CacheHandler instance, checking pending promises, evaluating LRU memory store entries, validating expiration timestamps or tags (areTagsExpired, areTagsStale), and teeing the underlying ReadableStream for safe consumption.
Sources: packages/next/src/server/lib/cache-handlers/default.tsSources: packages/next/src/server/use-cache/use-cache-wrapper.ts, packages/next/src/server/use-cache/handlers.ts, packages/next/src/server/lib/cache-handlers/default.ts
The wrapper inspects the active workUnitStore.type to enforce rules regarding where 'use cache' and 'use cache: private' expressions can execute.
Warning
Using 'use cache: private' within an unstable_cache() block or a nested public 'use cache' function throws an InvalidDynamicUsageError during execution because private state cannot be safely nested inside shared caching boundaries.
Sources: packages/next/src/server/use-cache/use-cache-wrapper.ts
Sources: packages/next/src/server/use-cache/handlers.ts, packages/next/src/server/lib/cache-handlers/default.ts, packages/next/src/server/lib/cache-handlers/default.ts
Cache handler initialization and resolution manage how Next.js sets up, loads, and routes requests to default or custom cache handlers within Node.js server runtimes. Handlers are stored globally using specific symbols (@next/cache-handlers, @next/cache-handlers-map, @next/cache-handlers-set, and @next/cache-handlers-private) to guarantee that identical cache instances remain accessible across different module copies and boundary lines.
Sources: packages/next/src/server/use-cache/handlers.ts
The initialization sequence sets up global maps and fallback handlers, loading user-defined modules when specified in the runtime configuration.
Sources: packages/next/src/server/route-modules/route-module.ts, packages/next/src/server/use-cache/handlers.ts, packages/next/src/server/next-server.ts
The call-chain for handler loading proceeds through distinct phases: constructor triggers unstable_preloadEntries, which calls loadCustomCacheHandlers, invoking initializeCacheHandlers, followed by set and resolvePending.
constructor initiates server lifecycle setup and triggers preloading when not in development mode.
Sources: packages/next/src/server/next-server.tsunstable_preloadEntries awaits prepare() and invokes custom cache handlers before preloading components.
Sources: packages/next/src/server/next-server.tsloadCustomCacheHandlers checks cacheHandlers configuration and checks whether initialization has occurred.
Sources: packages/next/src/server/route-modules/route-module.tsinitializeCacheHandlers allocates the global handlers map and sets up default or symbol-backed handlers.
Sources: packages/next/src/server/use-cache/handlers.tsset (via setCacheHandler) populates the cache handlers map and set with resolved custom handlers.
Sources: packages/next/src/server/use-cache/handlers.tsresolvePending settles active promises inside pendingSets once storage operations complete.
Sources: packages/next/src/server/lib/cache-handlers/default.tsWarning
Calling getCacheHandler(), getPrivateCacheHandler(), or setCacheHandler() before initializeCacheHandlers() has executed will throw an explicit error stating 'Cache handlers not initialized'.
Sources: packages/next/src/server/use-cache/handlers.ts, packages/next/src/server/use-cache/handlers.ts
Sources: packages/next/src/server/use-cache/handlers.ts, packages/next/src/server/use-cache/handlers.ts
Note
The private cache handler is intentionally stored outside the kind-keyed map on [privateHandlerSymbol] so that user-configured custom cache handlers can never inadvertently intercept or replace request-specific private cache storage.
Sources: packages/next/src/server/use-cache/handlers.ts
Sources: packages/next/src/server/use-cache/handlers.ts, packages/next/src/server/route-modules/route-module.ts, packages/next/src/server/lib/cache-handlers/default.ts
The EdgeRouteModuleWrapper class manages route execution specifically within the edge runtime. During an incoming request handling cycle, it initializes cache handlers using configuration retrieved from the route module and binds custom cache handlers before invoking the underlying route module handler.
Sources: packages/next/src/server/web/edge-route-module-wrapper.ts
Sources: packages/next/src/server/web/edge-route-module-wrapper.ts, packages/next/src/server/use-cache/handlers.ts, packages/next/src/server/use-cache/handlers.ts, packages/next/src/server/lib/cache-handlers/default.ts
handler — The private handler method executes upon receiving an incoming NextRequestHint and NextFetchEvent, fetches edge configuration via this.routeModule.getNextConfigEdge(), and calls initializeCacheHandlers(nextConfig.cacheMaxMemorySize).
Sources: packages/next/src/server/web/edge-route-module-wrapper.tsinitializeCacheHandlers — Allocates the global handlers map on globalThis, seeds default or remote handlers, and configures fallback mechanisms.
Sources: packages/next/src/server/use-cache/handlers.tsset — Iterates over this.cacheHandlers entries within the edge wrapper, invoking setCacheHandler(kind, cacheHandler) to bind each custom handler into the global map and set.
Sources: packages/next/src/server/web/edge-route-module-wrapper.ts, packages/next/src/server/use-cache/handlers.tsresolvePending — When cache set operations occur within cache handlers, active pendingSets promises are registered and subsequently resolved via resolvePending() once stream chunk reading and caching finalize.
Sources: packages/next/src/server/lib/cache-handlers/default.tsWarning
The edge runtime does not support Cache Components or static page generation; useCacheTimeout and staticPageGenerationTimeout are explicitly set to 0 as sentinels to surface configuration bugs immediately if ever read.
Sources: packages/next/src/server/web/edge-route-module-wrapper.ts
The default in-memory cache handler provides local, LRU-backed storage for cached function outputs and React Flight streams. When configured with a maximum memory size (maxSize), it instantiates an internal LRUCache instance that sizes entries based on the byte length of their underlying ReadableStream chunks. If maxSize is set to 0, the handler short-circuits to bypass memory allocation entirely, returning resolved undefined or no-op promises for all operations.
Sources: packages/next/src/server/lib/cache-handlers/default.ts
To prevent duplicate concurrent execution during cache population, the default cache handler tracks active asynchronous writes using a pendingSets map keyed by cacheKey. When a set operation begins, it generates a new Promise and stores its resolver in pendingSets. Incoming get requests check this map and await the pending promise before querying the underlying LRU store, ensuring that concurrent requests coalesce onto a single active write operation.
Sources: packages/next/src/server/lib/cache-handlers/default.ts, packages/next/src/server/lib/cache-handlers/default.ts
Cache entry validity is governed by timestamps, age thresholds, and associated cache tags managed via the tags manifest. The get implementation evaluates whether an entry has expired based on environment runtime targets: production environments drop entries once they pass their revalidate window, whereas development servers (next dev) serve stale entries until their absolute expire time is reached, relying on stale-while-revalidate wrappers to trigger background refreshing.
Sources: packages/next/src/server/lib/cache-handlers/default.ts, packages/next/src/server/lib/cache-handlers/default.ts
Note
Tag validation inspects both areTagsExpired and areTagsStale. If any associated tag is expired, the cache entry is treated as a cache miss (undefined), whereas stale tags downgrade the effective revalidate value to -1 to signal stale-while-revalidate behavior.
Sources: packages/next/src/server/lib/cache-handlers/default.ts
During development, Next.js implements a hang-detection probe pool and worker scheduling architecture to diagnose and resolve deadlocks during 'use cache' execution. When a cache fill operation stalls or hangs beyond a configured threshold, the main server process dispatches a probe request to an isolated background worker thread or child process. This mechanism uses jest-worker to maintain a pool of four workers, ensuring that state exploration does not block the primary request-handling thread.
Sources: packages/next/src/server/dev/use-cache-probe-pool.ts, packages/next/src/server/dev/use-cache-probe-worker.ts
The execution walkthrough for a hang-detection probe spans the main thread pool and the worker module as follows: installUseCacheProbe() wires the global hook in use-cache-probe-globals.ts → when a fill stalls, runProbe() grabs an active pool via getPool() → activePool.probeUseCache(msg) is called with a serialized ProbeMessage → the worker executes probeUseCache() in use-cache-probe-worker.ts, which sets HTTP agent options, loads components via loadComponents(), retrieves the server module map via getServerModuleMap(), decodes arguments using decodeReply() or decodeReplyFromAsyncIterable(), builds a request store via buildProbeWorkStore(), and finally runs the wrapped 'use cache' function inside workAsyncStorage.run() and workUnitAsyncStorage.run().
Sources: packages/next/src/server/dev/use-cache-probe-pool.ts, packages/next/src/server/dev/use-cache-probe-worker.ts
Note
The probe worker runs without an outer render context. Because of this isolation, cache-scope fetches resolve normally, and the shared module scope can never accumulate a halted promise that would poison sibling probe tasks. Sources: packages/next/src/server/dev/use-cache-probe-pool.ts
Warning
The dev server drops and tears down the entire worker pool whenever onCacheInvalidation() fires (such as during HMR refreshes or route recompilations). Without this teardown, workers would retain stale require.cache and manifest bindings from previous code versions.
Sources: packages/next/src/server/dev/use-cache-probe-worker.ts, packages/next/src/server/dev/use-cache-probe-pool.ts
Sources: packages/next/src/server/dev/use-cache-probe-worker.ts, packages/next/src/server/dev/use-cache-probe-pool.ts, packages/next/src/server/dev/use-cache-probe-pool.ts
The caching infrastructure interoperates with legacy primitives such as unstable_cache, patched native fetch operations, and coalesced function invocations. These mechanisms interact through unified incremental cache layers, shared asynchronous storage boundaries, and request stores.
Sources: packages/next/src/server/web/spec-extension/unstable-cache.ts, packages/next/src/lib/coalesced-function.ts
unstable_cache Execution and RevalidationThe unstable_cache wrapper intercepts expensive operations by combining a fixed function key with serialized arguments, fetching from or updating the underlying IncrementalCache.
Sources: packages/next/src/server/web/spec-extension/unstable-cache.ts
export function unstable_cache<T extends Callback>(
cb: T,
keyParts?: string[],
options: {
revalidate?: number | false
tags?: string[]
} = {}
): TDuring execution, unstable_cache performs the following call-chain sequence:
workAsyncStorage.getStore() and workUnitAsyncStorage.getStore() are queried to retrieve the active request and work unit contexts.
Sources: packages/next/src/server/web/spec-extension/unstable-cache.tsincrementalCache.generateCacheKey(invocationKey) builds the final hashed key from the fixed key and argument string.
Sources: packages/next/src/server/web/spec-extension/unstable-cache.tsworkStore.incrementalCache or globalThis.__incrementalCache is consulted to retrieve stored entries or schedule background revalidations via workStore.pendingRevalidates[invocationKey].
Sources: packages/next/src/server/web/spec-extension/unstable-cache.ts, packages/next/src/server/web/spec-extension/unstable-cache.tsworkUnitAsyncStorage.run(innerCacheStore, cb, ...args) runs the underlying callback within an unstable-cache work unit store, persisting the result using cacheNewResult().
Sources: packages/next/src/server/web/spec-extension/unstable-cache.ts, packages/next/src/server/web/spec-extension/unstable-cache.tsWarning
Passing revalidate: 0 to unstable_cache() throws an invariant error; revalidation must be either false or a positive number greater than zero.
Sources: packages/next/src/server/web/spec-extension/unstable-cache.ts
Patched fetchers integrate with work stores and incremental caches to store network response payloads as cached fetch data entries. Sources: packages/next/src/server/lib/patch-fetch.ts, packages/next/src/server/lib/patch-fetch.ts
export function createPatchedFetcher(
originFetch: Fetcher,
{ workAsyncStorage, workUnitAsyncStorage }: PatchableModule
): PatchedFetcherDuring dynamic rendering, createCachedDynamicResponse clones response streams, converts array buffers into base64-encoded body strings, and associates them with serverComponentsHmrCache and incremental cache instances while deduplicating simultaneous set operations through workStore.pendingRevalidates.
Sources: packages/next/src/server/lib/patch-fetch.ts
Concurrent identical function executions are deduplicated using withCoalescedInvoke, which maintains a global in-memory promise map.
Sources: packages/next/src/lib/coalesced-function.ts
export function withCoalescedInvoke<F extends (...args: any) => any>(
func: F
): (
key: string,
args: Parameters<F>
) => Promise<CoalescedInvoke<UnwrapPromise<ReturnType<F>>>>When an invocation occurs, withCoalescedInvoke checks globalInvokeCache for an existing promise mapped to the key. If an entry exists, subsequent callers receive a cloned promise resolving with isOrigin: false. If no entry exists, a wrapper executes func.apply(undefined, args), registers the pending promise in globalInvokeCache, and deletes the key upon settlement or rejection.
Sources: packages/next/src/lib/coalesced-function.ts
Note
withCoalescedInvoke cleans up its internal tracking map immediately upon promise resolution or rejection in both .then() and .catch() blocks, preventing permanent memory leaks for transient operations.
Sources: packages/next/src/lib/coalesced-function.ts