Skip to main content
doc0blade47/next.jsdocumentation home
Sign in

Incremental Cache

Last updated Oct 12, 2026100% grounded
Relevant source files

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

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.

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 PropertyTypeDescription
fsCacheFs | undefinedVirtual or node file system interface for disk storage.
devboolean | undefinedIndicates whether the server is running in development mode.
flushToDiskboolean | undefinedDetermines whether ISR updates should be written immediately to disk.
serverDistDirstring | undefinedAbsolute path to the server distribution directory.
maxMemoryCacheSizenumber | undefinedMaximum memory allowed for in-memory caching layers.
fetchCacheKeyPrefixstring | undefinedOptional prefix applied to all fetch cache storage keys.
prerenderManifestPrerenderManifest | undefinedManifest containing static routes, dynamic routes, and ISR revalidation timings.
revalidatedTagsstring[]List of tags marked as revalidated during the current execution.
_requestHeadersRecord<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.

      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, 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.

      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, 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 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.

  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.

  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.

      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.

             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.

              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.

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 MethodBehavior 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.

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.

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.

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]

FunctionComparison LogicReturn 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.

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 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.

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]

Was this page helpful?