---
title: "Incremental Cache"
description: "The incremental cache system in Next.js serves as the central coordination layer for persisting, retrieving, and revalidating cached application data, route responses, and server component executio..."
last_updated: "2026-10-12T06:38:56.881153+00:00"
canonical_url: "https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/incremental-cache"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/next/src/server/lib/incremental-cache/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/incremental-cache/index.ts)
- [packages/next/src/server/use-cache/use-cache-wrapper.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/use-cache/use-cache-wrapper.ts)
- [packages/next/src/server/lib/incremental-cache/file-system-cache.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/incremental-cache/file-system-cache.ts)
- [packages/next/src/server/response-cache/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/response-cache/index.ts)
- [packages/next/src/client/components/segment-cache/cache.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/components/segment-cache/cache.ts)
- [packages/next/src/server/web/spec-extension/unstable-cache.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/unstable-cache.ts)
- [packages/next/src/server/response-cache/types.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/response-cache/types.ts)
- [packages/next/src/server/lib/cache-handlers/default.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/cache-handlers/default.ts)
- [packages/next/src/server/route-modules/route-module.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/route-modules/route-module.ts)
- [packages/next/src/server/revalidation-utils.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/revalidation-utils.ts)
- [packages/next/src/server/lib/patch-fetch.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/patch-fetch.ts)
- [packages/next/src/server/async-storage/work-store.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/async-storage/work-store.ts)
- [packages/next/src/server/web/spec-extension/revalidate.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/revalidate.ts)
- [packages/next/src/server/lib/incremental-cache/tags-manifest.external.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/incremental-cache/tags-manifest.external.ts)
- [packages/next/src/server/resume-data-cache/resume-data-cache.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/resume-data-cache/resume-data-cache.ts)
</details>

## Overview

The incremental cache system in Next.js serves as the central coordination layer for persisting, retrieving, and revalidating cached application data, route responses, and server component execution outputs[[packages/next/src/server/lib/incremental-cache/index.ts:159-172], [packages/next/src/server/response-cache/index.ts:118-152], [packages/next/src/server/use-cache/use-cache-wrapper.ts:235-276]]. By unifying disparate caching mechanisms—including patched global `fetch`, unstable_cache, and the `'use cache'` runtime directive—it eliminates redundant computation and data fetching across requests while supporting both static rendering and incremental static regeneration (ISR)[[packages/next/src/server/lib/incremental-cache/index.ts:501-654], [packages/next/src/server/web/spec-extension/unstable-cache.ts:160-323], [packages/next/src/server/lib/cache-handlers/default.ts:74-152]]. The architecture separates storage adapters, such as file-system and in-memory LRU backends, from core orchestration logic, enabling configurable persistence strategies and efficient tag-based invalidation across server instances[[packages/next/src/server/lib/incremental-cache/file-system-cache.ts:38-69], [packages/next/src/server/lib/incremental-cache/tags-manifest.external.ts:10-42], [packages/next/src/server/lib/cache-handlers/default.ts:50-68]]. Sources: [packages/next/src/server/lib/incremental-cache/index.ts:159-172], [packages/next/src/server/lib/incremental-cache/file-system-cache.ts:38-69], [packages/next/src/server/lib/cache-handlers/default.ts:50-68], [packages/next/src/server/response-cache/index.ts:118-152], [packages/next/src/server/web/spec-extension/unstable-cache.ts:160-323], [packages/next/src/server/lib/incremental-cache/tags-manifest.external.ts:10-42]

## Incremental cache core architecture

### Public API and `CacheHandler` lifecycle

The `IncrementalCache` class orchestrates data retrieval and storage across fetch requests, route responses, and image generation. It interacts with storage backends via the `CacheHandler` base class, which defines the standard asynchronous interface for persistence operations.

```typescript
export class CacheHandler {
  constructor(_ctx: CacheHandlerContext) {}

  public async get(
    _cacheKey: string,
    _ctx:
      | GetIncrementalFetchCacheContext
      | GetIncrementalResponseCacheHandlerContext
      | GetIncrementalImageCacheContext
  ): Promise<CacheHandlerValue | null> {
    return {} as any
  }

  public async set(
    _cacheKey: string,
    _data: IncrementalCacheValue | null,
    _ctx:
      | SetIncrementalFetchCacheContext
      | SetIncrementalResponseCacheHandlerContext
      | SetIncrementalImageCacheContext
  ): Promise<void> {}

  public async revalidateTag(
    _tags: string | string[],
    _durations?: { expire?: number }
  ): Promise<void> {}

  public resetRequestCache(): void {}
}
```
Sources: [packages/next/src/server/lib/incremental-cache/index.ts:113-142]

The base `CacheHandler` accepts a `CacheHandlerContext` object during initialization, which supplies file system utilities, development flags, memory cache limits, and prerender manifests.

| Context Property | Type | Description |
| :--- | :--- | :--- |
| `fs` | `CacheFs \| undefined` | Virtual or node file system interface for disk storage. |
| `dev` | `boolean \| undefined` | Indicates whether the server is running in development mode. |
| `flushToDisk` | `boolean \| undefined` | Determines whether ISR updates should be written immediately to disk. |
| `serverDistDir` | `string \| undefined` | Absolute path to the server distribution directory. |
| `maxMemoryCacheSize` | `number \| undefined` | Maximum memory allowed for in-memory caching layers. |
| `fetchCacheKeyPrefix` | `string \| undefined` | Optional prefix applied to all fetch cache storage keys. |
| `prerenderManifest` | `PrerenderManifest \| undefined` | Manifest containing static routes, dynamic routes, and ISR revalidation timings. |
| `revalidatedTags` | `string[]` | List of tags marked as revalidated during the current execution. |
| `_requestHeaders` | `Record<string, undefined \| string \| string[]>` | Incoming HTTP request headers associated with the cache context. |

Sources: [packages/next/src/server/lib/incremental-cache/index.ts:43-53]

### Initialization and custom cache handler loading

Route modules initialize the incremental cache per request by invoking `getIncrementalCache` on incoming requests. This method detects the execution environment, loads user-defined custom cache handlers from `nextConfig.cacheHandlers`, and configures directory paths.

```typescript
      const incrementalCache = new IncrementalCache({
        fs: (
          require('../lib/node-fs-methods') as typeof import('../lib/node-fs-methods')
        ).nodeFs,
        dev: this.isDev,
        requestHeaders: req.headers,
        allowedRevalidateHeaderKeys:
          nextConfig.experimental.allowedRevalidateHeaderKeys,
        minimalMode: isMinimalMode,
        serverDistDir: `${projectDir}/${this.distDir}/server`,
        fetchCacheKeyPrefix: nextConfig.experimental.fetchCacheKeyPrefix,
        maxMemoryCacheSize: nextConfig.cacheMaxMemorySize,
        flushToDisk: !isMinimalMode && nextConfig.experimental.isrFlushToDisk,
        previewProps,
        prerenderManifest,
        CurCacheHandler: CacheHandler,
        locales: nextConfig.i18n?.locales,
      })
      ;(globalThis as any).__incrementalCache = incrementalCache
      return incrementalCache
```
Sources: [packages/next/src/server/route-modules/route-module.ts:546-568]

> [!NOTE]
> In the [Edge runtime](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/edge-runtime), `getIncrementalCache` bypasses dynamic class loading and returns the global instance stored at `globalThis.__incrementalCache`.
Sources: [packages/next/src/server/route-modules/route-module.ts:518-520]

### Retrieval orchestration and resume data cache integration

When `IncrementalCache.get()` is invoked with `IncrementalCacheKind.FETCH`, it first checks the Request Resume Data Cache (RDC) available through `workUnitAsyncStorage`. If a valid RDC entry exists and no associated tags have been recently revalidated, it returns the cached entry immediately without calling the underlying `cacheHandler`.

```typescript
      const workUnitStore = workUnitAsyncStorage.getStore()
      const resumeDataCache = workUnitStore
        ? getResumeDataCache(workUnitStore)
        : null
      if (resumeDataCache) {
        const memoryCacheData = resumeDataCache.fetch.get(cacheKey)
        if (memoryCacheData?.kind === CachedRouteKind.FETCH) {
          const workStore = workAsyncStorage.getStore()
          const combinedTags = [...(ctx.tags || []), ...(ctx.softTags || [])]
          const hasRevalidatedTag = combinedTags.some(
            (tag) =>
              this.revalidatedTags?.includes(tag) ||
              workStore?.pendingRevalidatedTags?.some(
                (item) => item.tag === tag
              )
          )

          if (hasRevalidatedTag) {
            // Fall through to cacheHandler lookup
          } else {
            return { isStale: false, value: memoryCacheData }
          }
        }
      }
```
Sources: [packages/next/src/server/lib/incremental-cache/index.ts:521-553]

> [!WARNING]
> If development mode is active and `cache-control` is set to `no-cache` on fetch requests, or if test mode is enabled via `disableForTestmode`, the prerender cache is entirely bypassed and `null` is returned.
Sources: [packages/next/src/server/lib/incremental-cache/index.ts:564-573]

For non-fetch entries, such as pages and [route handlers](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/route-handlers), `IncrementalCache` scopes storage keys using `getRouteCacheKey` before delegating retrieval to the configured `cacheHandler`.
Sources: [packages/next/src/server/lib/incremental-cache/index.ts:575-584]

## File system and memory backends

### File system cache storage and seed promotions

`FileSystemCache` coordinates persistent ISR and fetch data stored on disk with an in-memory `LRUCache` layer initialized when `ctx.maxMemoryCacheSize` is specified. Cache directory paths are resolved via `getFilePath`, directing fetch entries to `.next/cache/fetch-cache`, legacy pages to `.next/server/pages`, and [app router](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/client-runtime/app-router) outputs to `.next/server/app`. When reading scoped response keys starting with `/${ROUTE_CACHE_DIRECTORY}/`, `FileSystemCache` falls back to legacy path structures and verifies route metadata ownership. 

> [!NOTE]
> During seed reads, lookup operations track active readers and versions in `FileSystemCache.seedReads`. When a seed entry is successfully resolved, `promoteSeed` promotes it atomically to disk via `writeFileAtomic` and populates the memory cache layer.
Sources: [packages/next/src/server/lib/incremental-cache/file-system-cache.ts:38-49], [packages/next/src/server/lib/incremental-cache/file-system-cache.ts:73-96], [packages/next/src/server/lib/incremental-cache/file-system-cache.ts:449-492], [packages/next/src/server/lib/incremental-cache/file-system-cache.ts:742-778]

### Multi-file writer and memory cache backends

To serialize concurrent disk writes safely across multiple cache entries, `FileSystemCache` relies on `MultiFileWriter`. The default in-memory cache handler (`createDefaultCacheHandler`) manages local runtime caching using an instance of `LRUCache` that computes entry sizes from buffer byte lengths and cache key lengths.

```typescript
  const memoryCache = new LRUCache<PrivateCacheEntry>(
    maxSize,
    (entry, cacheKey) => entry.size + cacheKey.length
  )
  const pendingSets = new Map<string, Promise<void>>()
```
Sources: [packages/next/src/server/lib/cache-handlers/default.ts:64-68]

> [!WARNING]
> In production environments, cache entries configured with an expiration of zero (`expire === 0`) are treated as dynamic and skipped on write to prevent persisting values that will never be served back from the cache handler.
Sources: [packages/next/src/server/lib/cache-handlers/default.ts:166-176]

## Response cache integration

### In-flight batching and minimal mode LRU caching

`ResponseCache` manages route response caching, ISR entry resolution, and in-flight request deduplication across concurrent renders using two distinct batchers: `getBatcher` and `revalidateBatcher`. `getBatcher` uses a cache key function combining the route key with `isOnDemandRevalidate` (`${key}-${isOnDemandRevalidate ? '1' : '0'}`), while `schedulerFn` uses `scheduleOnNextTick` to ensure promises are added to pending collections before any asynchronous work begins.

```typescript
  private readonly getBatcher = Batcher.create<
    { key: string; isOnDemandRevalidate: boolean },
    IncrementalResponseCacheResult,
    string
  >({
    cacheKeyFn: ({ key, isOnDemandRevalidate }) =>
      `${key}-${isOnDemandRevalidate ? '1' : '0'}`,
    schedulerFn: scheduleOnNextTick,
  })

  private readonly revalidateBatcher = Batcher.create<
    string,
    IncrementalResponseCacheResult
  >({
    schedulerFn: scheduleOnNextTick,
  })
```
Sources: [packages/next/src/server/response-cache/index.ts:122-145]

In minimal mode, `ResponseCache` utilizes an `LRUCache` indexed by compound cache keys formed via `createCacheKey`, joining the pathname and `invocationID` (or `TTL_SENTINEL` when `invocationID` is absent). Evicted invocation IDs are tracked in `evictedInvocationIDs`, which is bounded to 100 entries using a FIFO policy to prevent unbounded memory growth.

> [!NOTE]
- If a prefetch request misses the cache, it bypasses `revalidateBatcher` to execute `handleRevalidate` directly. This prevents a prefetch from joining a background ISR fallback-shell upgrade and incorrectly serving a concrete page shell instead of the expected fallback state.
Sources: [packages/next/src/server/response-cache/index.ts:108-114], [packages/next/src/server/response-cache/index.ts:198-215], [packages/next/src/server/response-cache/index.ts:414-434]

### ISR entry resolution and stale checks

When processing a `get` request, `ResponseCache` checks the underlying `incrementalCache` (when not in minimal mode) to retrieve any existing `IncrementalResponseCacheEntry`. If `previousIncrementalCacheEntry` is present and its `isStale` property is not `-1` (which signals a blocking revalidate is required due to an expired tag or expiration time), the cache resolves immediately with the cached value.

```typescript
      if (
        previousIncrementalCacheEntry &&
        !context.isOnDemandRevalidate &&
        previousIncrementalCacheEntry.isStale !== -1
      ) {
        resolve(previousIncrementalCacheEntry)
        resolved = true

        if (!previousIncrementalCacheEntry.isStale || context.isPrefetch) {
          return previousIncrementalCacheEntry
        }
      }
```
Sources: [packages/next/src/server/response-cache/index.ts:377-401]

> [!WARNING]
> A value of `isStale === -1` forces a blocking revalidation, ignoring non-blocking stale cache entries so that users receive a freshly generated response rather than an expired tag or timed-out cache payload.
Sources: [packages/next/src/server/response-cache/index.ts:384-393]

## Fetch patching and unstable cache

### Global fetch patching and request caching

Next.js intercepts global network requests through a patched `fetch` implementation that integrates with the Incremental Cache and work unit stores. When a fetch request occurs during rendering, the patched function checks whether the request is cacheable and attempts to retrieve an existing entry using `incrementalCache.get`. If a valid cache entry is found and its state is stale, the request handles revalidation based on the consumer requirements.

```typescript
             if (entry?.value && entry.value.kind === CachedRouteKind.FETCH) {
               // If the consumer will persist this result in a server cache,
               // wait for fresh data so it doesn't persist a stale value.
               if (willConsumerServerCache(workUnitStore) && entry.isStale) {
                 isForegroundRevalidate = true
               } else {
                 if (entry.isStale) {
                   workStore.pendingRevalidates ??= {}
                   if (!workStore.pendingRevalidates[cacheKey]) {
                     const pendingRevalidate = doOriginalFetch(true)
                       .then(async (response) => ({
                         body: await response.arrayBuffer(),
                         headers: response.headers,
                         status: response.status,
                         statusText: response.statusText,
                       }))
                       .finally(() => {
                         workStore.pendingRevalidates ??= {}
                         delete workStore.pendingRevalidates[cacheKey || '']
                       })

                     pendingRevalidate.catch(console.error)

                     workStore.pendingRevalidates[cacheKey] = pendingRevalidate
                   }
                 }

                 cachedFetchData = entry.value.data
               }
             }
```
Sources: [packages/next/src/server/lib/patch-fetch.ts:1114-1145]

> [!WARNING]
> When `init.cache` is set to `'no-store'` in the Edge Runtime, the `cache` property is deleted from the options object before execution to prevent errors from platforms like Cloudflare Workers that do not support standard cache configuration flags.
Sources: [packages/next/src/server/lib/patch-fetch.ts:1177-1186]

### The `unstable_cache` wrapper and background revalidation

The `unstable_cache` helper wraps asynchronous data-fetching functions to cache their return values across invocations. Within an App Router context, `unstable_cache` inspects `workUnitStore` types to aggregate cache tags and manage revalidation intervals. If an entry is stale, it initiates a background revalidation promise stored within `workStore.pendingRevalidates` to avoid duplicate concurrent executions.

```typescript
              if (cacheEntry.isStale) {
                if (!workStore.pendingRevalidates) {
                  workStore.pendingRevalidates = {}
                }

                if (!workStore.pendingRevalidates[invocationKey]) {
                  const revalidationPromise = workUnitAsyncStorage
                    .run(innerCacheStore, cb, ...args)
                    .then(async (result) => {
                      await cacheNewResult(
                        result,
                        incrementalCache,
                        cacheKey,
                        tags,
                        revalidate,
                        fetchIdx,
                        fetchUrl
                      )
                      return result
                    })
                    .catch((err) => {
                      console.error(
                        `revalidating cache with key: ${invocationKey}`,
                        err
                      )
                      return cachedResponse
                    })

                  if (willConsumerServerCache(workUnitStore)) {
                    revalidationPromise.catch(() => {})
                  }

                  workStore.pendingRevalidates[invocationKey] =
                    revalidationPromise
                }

                if (willConsumerServerCache(workUnitStore)) {
                  return await workStore.pendingRevalidates[invocationKey]
                }
                return cachedResponse
              }
```
Sources: [packages/next/src/server/web/spec-extension/unstable-cache.ts:264-321]

> [!NOTE]
> Work unit store types including `cache`, `private-cache`, `prerender`, `prerender-runtime`, and `prerender-legacy` participate in time-to-live reduction by comparing and updating `workUnitStore.revalidate` against the narrower revalidation interval specified by individual cache wrappers.
Sources: [packages/next/src/server/web/spec-extension/unstable-cache.ts:182-198]

## Use cache function execution

### Tiered handler orchestration and default memory cache

Cache handler functions coordinate through dedicated storage layers, managed via handlers such as `getCacheHandler`, `getDevTieredCacheHandler`, and `getPrivateCacheHandler`. The default implementation provided by `createDefaultCacheHandler` builds upon an in-memory `LRUCache` instance keyed by serialized entries and cache keys.

```typescript
export function createDefaultCacheHandler(maxSize: number): CacheHandler {
  // If the max size is 0, return a cache handler that doesn't cache anything,
  // this avoids an unnecessary LRUCache instance and potential memory
  // allocation.
  if (maxSize === 0) {
    return {
      get: () => Promise.resolve(undefined),
      set: () => Promise.resolve(),
      refreshTags: () => Promise.resolve(),
      getExpiration: () => Promise.resolve(0),
      updateTags: () => Promise.resolve(),
    }
  }

  const memoryCache = new LRUCache<PrivateCacheEntry>(
    maxSize,
    (entry, cacheKey) => entry.size + cacheKey.length
  )
  const pendingSets = new Map<string, Promise<void>>()

  const debug = process.env.NEXT_PRIVATE_DEBUG_CACHE
    ? console.debug.bind(console, 'DefaultCacheHandler:')
    : undefined

```
Sources: [packages/next/src/server/lib/cache-handlers/default.ts:50-73]

> [!NOTE]
> If `maxSize` is configured to `0`, `createDefaultCacheHandler` immediately returns a dummy cache handler interface whose methods resolve to `undefined` or empty promises, bypassing LRU allocation entirely.
Sources: [packages/next/src/server/lib/cache-handlers/default.ts:54-62]

| Handler Method | Behavior on Invocation |
| :--- | :--- |
| `get(cacheKey)` | Awaits any pending set operation, retrieves from the memory LRU, checks eviction sentinels and tags, and streams back the buffer payload. |
| `set(cacheKey, pendingEntry)` | Registers a pending set promise to serialize the incoming stream to a Buffer, writing it into the LRU cache unless skipped as a dynamic entry. |
| `getExpiration(tags)` | Scans the tag manifest for entries, returning the maximum expired timestamp among requested tags or `0`. |
| `updateTags(tags, durations)` | Updates the tag manifest with immediate expiration or computed stale/expired timestamps derived from provided durations. |

Sources: [packages/next/src/server/lib/cache-handlers/default.ts:75-244]

### Streaming serialization and shared cache state

In `packages/next/src/server/use-cache/use-cache-wrapper.ts`, execution is coordinated via `SharedCacheEntry` and `ResolvableSharedCacheResult` classes to deduplicate concurrent cache invocations and manage stream forking.

```typescript
class SharedCacheEntry {
  private stream: ReadableStream<Uint8Array>

  /**
   * Resolves when collection finishes, without waiting for handler writes.
   */
  public readonly pendingMetadata: Promise<CacheResultMetadata>

  /**
   * Resolves with collected metadata after any handler writes settle. Write
   * failures do not discard the metadata. Collection failures still reject this
   * promise.
   */
  public readonly pendingCompletion: Promise<CacheResultMetadata>

  constructor(
    stream: ReadableStream<Uint8Array>,
    pendingMetadata: Promise<CacheResultMetadata>,
    pendingWrite: Promise<void> | undefined
  ) {
    this.stream = stream
    this.pendingMetadata = pendingMetadata
    this.pendingCompletion =
      pendingWrite === undefined
        ? pendingMetadata
        : pendingWrite.then(
            () => pendingMetadata,
            () => pendingMetadata
          )
  }

  /**
   * Tee the stream: returns a copy for the caller, replaces the internal stream
   * with the remaining branch for future callers. Both the leader and joiners
   * call this — everyone gets a fork.
   */
  fork(): ReadableStream<Uint8Array> {
    const [forked, remaining] = this.stream.tee()
    this.stream = remaining
    return forked
  }
}
```
Sources: [packages/next/src/server/use-cache/use-cache-wrapper.ts:235-276]

> [!WARNING]
> Metadata propagation and stream reading are decoupled: `SharedCacheEntry` exposes `pendingMetadata` which resolves as soon as data collection finishes, whereas `pendingCompletion` waits for underlying handler write operations to settle.
Sources: [packages/next/src/server/use-cache/use-cache-wrapper.ts:238-264]

Concurrent executions join existing cache entries through `ResolvableSharedCacheResult`, ensuring that duplicate requests during generation share the same underlying stream via `.tee()` fork operations without triggering redundant work against origin handlers.

```typescript
class ResolvableSharedCacheResult {
  private readonly deferred = createPromiseWithResolvers<SharedCacheResult>()

  private readonly registrations: Array<{
    map: Map<string, Promise<SharedCacheResult>>
    key: string
    /**
     * Where to move the entry once the invocation completes, instead of
     * dropping it. Only set for the request-scoped map, and only for kinds
     * whose entries are worth retaining; the module-scope cross-request map
     * must never retain, since its entries would outlive the request that
     * produced them.
     */
    retentionMap?: Map<string, Promise<SharedCacheResult>>
  }> = []

  registerIn(
    map: Map<string, Promise<SharedCacheResult>>,
    key: string,
    retentionMap?: Map<string, Promise<SharedCacheResult>>
  ): void {
    map.set(key, this.deferred.promise)
    this.registrations.push({ map, key, retentionMap })
  }
```
Sources: [packages/next/src/server/use-cache/use-cache-wrapper.ts:302-325]

## Tag validation and revalidation lifecycle

### Expiration tracking and stale tag calculation

Tag validity is evaluated against the shared `tagsManifest` map, storing entries with `stale` and `expired` numeric timestamps. The helper functions `areTagsExpired` and `areTagsStale` inspect incoming tag arrays against a given `timestamp` baseline to determine cache validity.

```typescript
export const areTagsExpired = (tags: string[], timestamp: Timestamp) => {
  for (const tag of tags) {
    const entry = tagsManifest.get(tag)
    const expiredAt = entry?.expired

    if (typeof expiredAt === 'number') {
      const now = performance.timeOrigin + performance.now()
      // For immediate expiration (expiredAt <= now) and tag was invalidated after entry was created
      // OR for future expiration that has now passed (expiredAt > timestamp && expiredAt <= now)
      const isImmediatelyExpired = expiredAt <= now && expiredAt > timestamp

      if (isImmediatelyExpired) {
        return true
      }
    }
  }

  return false
}

export const areTagsStale = (tags: string[], timestamp: Timestamp) => {
  for (const tag of tags) {
    const entry = tagsManifest.get(tag)
    const staleAt = entry?.stale ?? 0

    if (typeof staleAt === 'number' && staleAt > timestamp) {
      return true
    }
  }

  return false
```
Sources: [packages/next/src/server/lib/incremental-cache/tags-manifest.external.ts:12-42]

> [!NOTE]
> An entry is considered immediately expired if `expiredAt <= now` and `expiredAt > timestamp`, or if a future expiration time has passed since the cached entry's creation timestamp.
Sources: [packages/next/src/server/lib/incremental-cache/tags-manifest.external.ts:17-25]

| Function | Comparison Logic | Return Condition |
| :--- | :--- | :--- |
| `areTagsExpired(tags, timestamp)` | Checks if `expiredAt <= now && expiredAt > timestamp` for any tag entry. | Returns `true` on the first expired match. |
| `areTagsStale(tags, timestamp)` | Checks if `staleAt > timestamp` for any tag entry in `tagsManifest`. | Returns `true` if any tag is newer than the cache entry timestamp. |

Sources: [packages/next/src/server/lib/incremental-cache/tags-manifest.external.ts:12-42]

### On-demand revalidation and work store propagation

On-demand revalidation APIs such as `revalidateTag`, `updateTag`, and `revalidatePath` queue tag invalidations within the active `workStore.pendingRevalidatedTags` array via `revalidate()`. During execution, `withExecuteRevalidates` snapshots the initial revalidation state, runs the callback, computes state differences through `diffRevalidationState`, and dispatches updates via `executeRevalidates`.

```typescript
export async function withExecuteRevalidates<T>(
  store: WorkStore | undefined,
  callback: () => Promise<T>
): Promise<T> {
  if (!store) {
    return callback()
  }
  // If we executed any revalidates during the request, then we don't want to execute them again.
  // save the state so we can check if anything changed after we're done running callbacks.
  const savedRevalidationState = cloneRevalidationState(store)
  try {
    return await callback()
  } finally {
    // Check if we have any new revalidates, and if so, wait until they are all resolved.
    const newRevalidates = diffRevalidationState(
      savedRevalidationState,
      cloneRevalidationState(store)
    )
    await executeRevalidates(store, newRevalidates)
  }
}
```
Sources: [packages/next/src/server/revalidation-utils.ts:6-26]

> [!WARNING]
> Calling `updateTag` outside of a Server Action throws an error requiring `revalidateTag` for Route Handlers, whereas `refresh` restricts dynamic path revalidation specifically to [Server Actions](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/server-actions) with action phase validation.
Sources: [packages/next/src/server/web/spec-extension/revalidate.ts:58-103]

Tags are grouped by their configured cache profiles for batch processing. The `revalidateTags` function iterates over resolved cache life configurations, invoking `handler.updateTags` across active cache handlers and `incrementalCache.revalidateTag`.

```typescript
async function revalidateTags(
  tagsWithProfile: Array<{
    tag: string
    profile?: string | { expire?: number }
  }>,
  incrementalCache: IncrementalCache | undefined,
  workStore?: WorkStore
): Promise<void> {
  if (tagsWithProfile.length === 0) {
    return
  }

  const handlers = getCacheHandlers() ?? []
  const promises: Promise<void>[] = []

  // Group tags by profile for batch processing
  const tagsByProfile = new Map<
    | string
    | { stale?: number; revalidate?: number; expire?: number }
    | undefined,
    string[]
  >()

  for (const item of tagsWithProfile) {
    const profile = item.profile
    // Find existing profile by comparing values
    let existingKey = undefined
    for (const [key] of tagsByProfile) {
      if (
        typeof key === 'string' &&
        typeof profile === 'string' &&
        key === profile
      ) {
        existingKey = key
        break
      }
      if (
        typeof key === 'object' &&
        typeof profile === 'object' &&
        JSON.stringify(key) === JSON.stringify(profile)
      ) {
        existingKey = key
        break
      }
      if (key === profile) {
        existingKey = key
        break
      }
    }

    const profileKey = existingKey || profile
    if (!tagsByProfile.has(profileKey)) {
      tagsByProfile.set(profileKey, [])
    }
    tagsByProfile.get(profileKey)!.push(item.tag)
  }

  // Process each profile group
  for (const [profile, tagsForProfile] of tagsByProfile) {
    // Look up the cache profile from workStore if available
    let durations: { expire?: number } | undefined

    if (profile) {
      let cacheLife:
        | { stale?: number; revalidate?: number; expire?: number }
        | undefined

      if (typeof profile === 'object') {
        // Profile is already a cacheLife configuration object
        cacheLife = profile
      } else if (typeof profile === 'string') {
        // Profile is a string key, look it up in workStore
        cacheLife = workStore?.cacheLifeProfiles[profile]

        if (!cacheLife) {
          throw new Error(
            `Invalid profile provided "${profile}" must be configured under cacheLife in next.config or be "max"`
          )
        }
      }
```
Sources: [packages/next/src/server/revalidation-utils.ts:88-192]

## Related

- [Server Architecture](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/server-architecture)
- [Dynamic Rendering](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/dynamic-rendering)
- [App Rendering](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/app-rendering)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/llms.txt) for all pages in this wiki.
