---
title: "Dynamic Rendering"
description: "Dynamic Rendering encompasses the mechanisms Next.js uses to track runtime data access, manage partial prerendering (PPR) shells, and defer or evaluate dynamic route segments, search parameters, an..."
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/dynamic-rendering"
---

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

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

- [packages/next/src/server/app-render/dynamic-rendering.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/dynamic-rendering.ts)
- [packages/next/src/build/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/index.ts)
- [packages/next/src/build/templates/app-page-runtime.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/app-page-runtime.ts)
- [packages/next/src/server/dynamic-rendering-utils.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dynamic-rendering-utils.ts)
- [packages/next/src/server/base-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-server.ts)
- [packages/next/src/build/adapter/build-complete.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/adapter/build-complete.ts)
- [packages/next/src/server/config-shared.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/config-shared.ts)
- [packages/next/src/server/route-modules/app-route/module.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/route-modules/app-route/module.ts)
- [packages/next/src/client/components/render-tree.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/components/render-tree.ts)
- [packages/next/src/server/app-render/dynamic-rendering-client.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/dynamic-rendering-client.ts)
- [packages/next/src/server/request/search-params.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/search-params.ts)
- [packages/next/src/server/request/draft-mode.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/draft-mode.ts)
- [packages/next/src/server/web/spec-extension/unstable-no-store.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/unstable-no-store.ts)
- [packages/next/src/client/components/segment-cache/scheduler.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/components/segment-cache/scheduler.ts)
- [packages/next/src/server/render.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/render.tsx)
- [packages/next/src/server/app-render/vary-params.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/vary-params.ts)
- [packages/next/src/server/request/params.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/params.ts)
- [packages/next/src/server/request/fallback-params.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/fallback-params.ts)
- [packages/next/src/server/app-render/dynamic-access-async-storage.external.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/dynamic-access-async-storage.external.ts)
- [packages/next/src/server/app-render/postponed-state.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/postponed-state.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

Dynamic Rendering encompasses the mechanisms Next.js uses to track runtime data access, manage partial prerendering (PPR) shells, and defer or evaluate dynamic route segments, search parameters, and draft mode states. By intercepting dynamic operations through [Async Storage](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/async-storage) execution contexts, the system coordinates [static generation](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/build-system/static-generation) bailouts, generates artificial hanging promises, and accumulates varying parameters to construct and restore suspended streams in the [App Rendering](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/app-rendering) pipeline.

At build time in the [Build Pipeline](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/build-system/build-pipeline), route entry configurations determine whether pages are fully static, dynamically rendered on demand, or split into static prerender shells with postponed dynamic holes. The [Server Architecture](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/server-architecture) consumes these manifests to govern cache keys, evaluate prefetch headers, and control stream output during live requests.

Sources: [packages/next/src/server/app-render/dynamic-rendering.ts:1282-1293](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/dynamic-rendering.ts#L1282-L1293), [packages/next/src/server/dynamic-rendering-utils.ts:342-351](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dynamic-rendering-utils.ts#L342-L351), [packages/next/src/server/app-render/postponed-state.ts:84-87](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/postponed-state.ts#L84-L87)

## Dynamic Tracking Mechanics

### Validation and Bailout Enforcement

The server evaluates execution paths using validation states and store checks to trigger static generation bailouts or runtime errors when disallowed dynamic access occurs. Functions such as `throwIfDisallowedDynamic` and `throwIfDisallowedDynamicInStaticRoute` inspect prelude states, dynamic error collections, and viewport flags to determine whether a route can produce a static shell or must terminate with a `StaticGenBailoutError`.

```typescript
export function throwIfDisallowedDynamicInStaticRoute(
  workStore: WorkStore,
  prelude: PreludeState,
  dynamicValidation: DynamicValidationState,
  serverDynamic: DynamicTrackingState,
  isFallbackShell: boolean,
  allowEmptyStaticShell: boolean,
  isServerPartial: boolean
): void {
  const reasons = getDisallowedReasonsInStaticRoute(
    workStore,
    prelude,
    dynamicValidation,
    serverDynamic,
    isFallbackShell,
    allowEmptyStaticShell,
    isServerPartial
  )
  if (reasons.length > 0) {
    for (const reason of reasons) {
      logDisallowedDynamicError(workStore, reason)
    }
    throw new StaticGenBailoutError()
  }
}
```
Sources: [packages/next/src/server/app-render/dynamic-rendering.ts:1366-1390](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/dynamic-rendering.ts#L1366-L1390)

> [!WARNING]
> If a route produces an empty prelude (`PreludeState.Empty`) without recorded dynamic errors during validation, Next.js treats the condition as an internal tracking failure and throws an `InvariantError` rather than a standard bailout.

Sources: [packages/next/src/server/app-render/dynamic-rendering.ts:1437-1447](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/dynamic-rendering.ts#L1437-L1447)

### Declarative Opt-Outs and Abort Controllers

Declarative opt-outs via `unstable_noStore` inspect active work stores and evaluate work unit store types to mark scopes as dynamic, though behavior varies depending on whether the execution context operates under dynamic I/O or standard rendering modes.

```typescript
export function unstable_noStore() {
  const callingExpression = 'unstable_noStore()'
  const store = workAsyncStorage.getStore()
  const workUnitStore = workUnitAsyncStorage.getStore()
  if (!store) {
    // This generally implies we are being called in Pages router. We should probably not support
    // unstable_noStore in contexts outside of `react-server` condition but since we historically
    // have not errored here previously, we maintain that behavior for now.
    return
  } else if (store.forceStatic) {
    return
  } else {
    store.isUnstableNoStore = true
    if (workUnitStore) {
      switch (workUnitStore.type) {
        case 'prerender':
        case 'prerender-client':
        case 'validation-client':
        case 'prerender-runtime':
          // unstable_noStore() is a noop in Dynamic I/O.
          return
        case 'prerender-legacy':
        case 'request':
        case 'cache':
        case 'private-cache':
        case 'unstable-cache':
        case 'build-time-generator':
          break
        default:
          workUnitStore satisfies never
      }
    }
    markCurrentScopeAsDynamic(store, workUnitStore, callingExpression)
  }
}
```
Sources: [packages/next/src/server/web/spec-extension/unstable-no-store.ts:20-54](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/unstable-no-store.ts#L20-L54)

| Work Unit Store Type | Behavior in `unstable_noStore()` |
| :--- | :--- |
| `prerender`, `prerender-client`, `validation-client`, `prerender-runtime` | No-op under Dynamic I/O |
| `prerender-legacy`, `request`, `cache`, `private-cache`, `unstable-cache`, `build-time-generator` | Marks current scope as dynamic |

Sources: [packages/next/src/server/web/spec-extension/unstable-no-store.ts:33-47](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/unstable-no-store.ts#L33-L47)

Additionally, `abortOnDynamicAccess` interacts with `dynamicAccessAsyncStorage` to abort ongoing fetches or cache reads when runtime or fallback parameter dependencies are detected.

```typescript
export function abortOnDynamicAccess(
  reason: DynamicAccessReason,
  error: Error
) {
  const store = dynamicAccessAsyncStorageInstance.getStore()
  if (store !== undefined) {
    // A cache may read both kinds of data before cancellation finishes. Only
    // fallback-only dependencies can become static when the params are known.
    if (store.reason !== 'runtime') {
      store.reason = reason
    }
    store.abortController.abort(error)
  }
}
```
Sources: [packages/next/src/server/app-render/dynamic-access-async-storage.external.ts:16-29](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/dynamic-access-async-storage.external.ts#L16-L29)

## Hanging Promises Implementation

### Artificial Unresolved Promises and Staged Tracking

When Next.js encounters dynamic hooks or runtime data access during pre-rendering or static generation stages, it creates artificial unresolved promises to suspend rendering until dynamic stages or client hydration occur. Functions such as `makeHangingPromise` and `makeUnknownRuntimeDataHangingPromise` construct promises tied to an abort signal and an error expression, optionally tracking URL data or session data access via `workUnitStore`.

```typescript
  expression: string,
  workUnitStore: WorkUnitStore | null
): Promise<T> {
  const promise = makeHangingPromiseWithError<T>(
    signal,
    new HangingPromiseRejectionError(route, expression)
  )
  if (workUnitStore === null) {
    return promise
  }
  return trackPromiseUsed(
    promise,
    trackURLDataAccessed.bind(null, workUnitStore, expression)
  )
}
```
Sources: [packages/next/src/server/dynamic-rendering-utils.ts:168-182](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dynamic-rendering-utils.ts#L168-L182)

Runtime data tracking relies on `PrerenderDataKind` constants to classify whether accessed data affects session storage, URL parameters, or fallback parameters. The internal `trackRuntimeDataAccessed` function inspects the active `workUnitStore` stage to determine whether to deopt the shell, the prefetch, or both.

```typescript
const enum PrerenderDataKind {
  SessionData = 1,
  UrlData = 2,
  FallbackParams = 3,
}
```
Sources: [packages/next/src/server/dynamic-rendering-utils.ts:353-357](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dynamic-rendering-utils.ts#L353-L357)

| Prerender Data Kind Value | Constant Name | Target Deopt Scope |
| :--- | :--- | :--- |
| `1` | `SessionData` | Shell and Prefetch |
| `2` | `UrlData` | Prefetch only |
| `3` | `FallbackParams` | Prefetch or transient upgradeable fallback |

Sources: [packages/next/src/server/dynamic-rendering-utils.ts:354-356](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dynamic-rendering-utils.ts#L354-L356), [packages/next/src/server/dynamic-rendering-utils.ts:402-485](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dynamic-rendering-utils.ts#L402-L485)

> [!NOTE]
> Accesses occurring after `RenderStage.NavigationStatic` are ignored because the purpose of runtime tracking is to evaluate whether a runtime prefetch provides more complete data than a static prefetch.

Sources: [packages/next/src/server/dynamic-rendering-utils.ts:379-387](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dynamic-rendering-utils.ts#L379-L387)

### Client Hook Hanging Promises and Work Unit Validation

On the client-side app rendering and routing context, functions like `useDynamicRouteParams` and `useDynamicSearchParams` evaluate active work unit store types to suspend component execution. When running inside a `prerender-client` context with cache component semantics, these hooks invoke `React.use()` with a hanging promise created via `makeClientHookHangingPromise`.

```typescript
export function useDynamicRouteParams(expression: string) {
  const workStore = workAsyncStorage.getStore()
  const workUnitStore = workUnitAsyncStorage.getStore()
  if (workStore && workUnitStore) {
    switch (workUnitStore.type) {
      case 'prerender-client': {
        const fallbackParams = workUnitStore.fallbackRouteParams

        if (fallbackParams && fallbackParams.size > 0) {
          // We are in a prerender with cacheComponents semantics. We are going to
          // hang here and never resolve. This will cause the currently
          // rendering component to effectively be a dynamic hole.
          React.use(
            makeClientHookHangingPromise(
              workUnitStore.renderSignal,
              new ClientHookDynamicError(workStore.route, expression)
            )
          )
        }
        break
```
Sources: [packages/next/src/server/app-render/dynamic-rendering-client.ts:27-46](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/dynamic-rendering-client.ts#L27-L46)

| Work Unit Store Type | `useDynamicRouteParams` Behavior | `useDynamicSearchParams` Behavior |
| :--- | :--- | :--- |
| `prerender-client` | Suspends via hanging promise if fallback parameters exist | Suspends via hanging promise |
| `prerender` / `prerender-runtime` | Throws `InvariantError` | Throws `InvariantError` |
| `validation-client` | Bypasses fallback route parameter checks | Bypasses hanging promise for validation |
| `cache` / `private-cache` / `unstable-cache` | Throws `InvariantError` | Throws `InvariantError` |
| `build-time-generator` | Throws `InvariantError` mentioning function name | Throws `InvariantError` mentioning function name |
| `request` | No-op | No-op |

Sources: [packages/next/src/server/app-render/dynamic-rendering-client.ts:31-73](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/dynamic-rendering-client.ts#L31-L73), [packages/next/src/server/app-render/dynamic-rendering-client.ts:93-135](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/dynamic-rendering-client.ts#L93-L135)

> [!WARNING]
> Calling search parameter or dynamic route parameter hooks from a server component during standard server prerendering triggers an `InvariantError`, as Next.js expects these expressions to be intercepted before server component inclusion.

Sources: [packages/next/src/server/app-render/dynamic-rendering-client.ts:48-51](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/dynamic-rendering-client.ts#L48-L51), [packages/next/src/server/app-render/dynamic-rendering-client.ts:119-123](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/dynamic-rendering-client.ts#L119-L123)

## Fallback Parameters Resolution

### Staged Fallback Parameters and Segment Placeholders

```typescript
export type OpaqueFallbackRouteParams = ReadonlyMap<string, string>

/**
 * Creates an opaque fallback route params object from the fallback route params.
 *
 * @param fallbackRouteParams the fallback route params
 * @returns the opaque fallback route params
 */
export function createOpaqueFallbackRouteParams(
  fallbackRouteParams: readonly FallbackRouteParam[]
): OpaqueFallbackRouteParams | null {
  // If there are no fallback route params, we can return early.
  if (fallbackRouteParams.length === 0) return null

  // As we're creating unique keys for each of the dynamic route params, we only
  // need to generate a unique ID once per request because each of the keys will
  // be also be unique.
  const uniqueID = Math.random().toString(16).slice(2)

  const keys = new Map<string, string>()

  // Generate a unique key for the fallback route param, if this key is found
  // in the static output, it represents a bug in cache components.
  for (const { paramName } of fallbackRouteParams) {
    keys.set(paramName, `%%drp:${paramName}:${uniqueID}%%`)
  }

  return keys
}
```
Sources: [packages/next/src/server/request/fallback-params.ts:13-41](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/fallback-params.ts#L13-L41)

> [!NOTE]
> Opaque fallback route parameters map unknown parameter names to unique randomized placeholder identifiers (`%%drp:${paramName}:${uniqueID}%%`). These placeholders prevent unprerendered dynamic segments from leaking into rendered UI during static shell generation.

Sources: [packages/next/src/server/request/fallback-params.ts:9-41](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/fallback-params.ts#L9-L41)

```typescript
export function getStagedFallbackParams(route: {
  fallbackRouteParams: readonly FallbackRouteParam[] | undefined
  remainingPrerenderableParams?: readonly FallbackRouteParam[]
  throwOnEmptyStaticShell?: boolean
}): OpaqueFallbackRouteParams | null {
  const { fallbackRouteParams, remainingPrerenderableParams } = route
  if (!fallbackRouteParams?.length) {
    return null
  }

  if (route.throwOnEmptyStaticShell === undefined) {
    throw new InvariantError(
      'Expected throwOnEmptyStaticShell for a route with fallback params'
    )
  }

  // A required shell keeps all of its fallback params deferred.
  if (route.throwOnEmptyStaticShell || !remainingPrerenderableParams?.length) {
    return createOpaqueFallbackRouteParams(fallbackRouteParams)
  }

  return createOpaqueFallbackRouteParams(
    fallbackRouteParams.filter(
      (param) =>
        !remainingPrerenderableParams.some(
          (prerenderableParam) =>
            prerenderableParam.paramName === param.paramName
        )
    )
  )
}
```
Sources: [packages/next/src/server/request/fallback-params.ts:56-86](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/fallback-params.ts#L56-L86)

| Helper Function Name | Input Signature | Purpose / Behavior |
| :--- | :--- | :--- |
| `createOpaqueFallbackRouteParams` | `fallbackRouteParams: readonly FallbackRouteParam[]` | Generates a randomized `OpaqueFallbackRouteParams` map assigning unique wrapper strings to each parameter name. |
| `getStagedFallbackParams` | `route: { fallbackRouteParams, remainingPrerenderableParams?, throwOnEmptyStaticShell? }` | Selects which fallback parameters are deferred for a given target shell based on build metadata and remaining prerenderable parameters. |
| `buildDynamicSegmentPlaceholder` | `param: Pick<FallbackRouteParam, 'paramName' \| 'paramType'>` | Constructs bracketed dynamic segment expressions based on parameter repeat and optional properties. |
| `getPlaceholderFallbackRouteParams` | `params: Record<string, any> \| undefined, fallbackRouteParams: readonly FallbackRouteParam[]` | Filters fallback parameters whose active values match generated dynamic segment placeholders. |
| `getFallbackRouteParams` | `page: string, routeModule: AppPageRouteModule` | Parses the normalized route, extracts pathname segments from the loader tree, and resolves unknown parameters. |

Sources: [packages/next/src/server/request/fallback-params.ts:21-165](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/fallback-params.ts#L21-L165)

> [!IMPORTANT]
> When `throwOnEmptyStaticShell` is true or no `remainingPrerenderableParams` exist, `getStagedFallbackParams` defers all fallback parameters because the required shell requires a non-empty prelude matching the exact validated shape.

Sources: [packages/next/src/server/request/fallback-params.ts:72-75](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/fallback-params.ts#L72-L75)

## Draft Mode Tracking

### Work Unit Scopes and Execution Contexts

```typescript
switch (workUnitStore.type) {
  case 'prerender-runtime': {
    // TODO(runtime-ppr): does it make sense to delay this? normally it's always microtasky
    const { stagedRendering } = workUnitStore
    if (stagedRendering) {
      return stagedRendering.delayUntilStage(
        RENDER_STAGES_BY_DATA_KIND.sessionData,
        'draftMode',
        new DraftMode(workUnitStore.draftMode)
      )
    } else {
      return createOrGetCachedDraftMode(workUnitStore.draftMode, workStore)
    }
  }
  case 'request':
    return createOrGetCachedDraftMode(workUnitStore.draftMode, workStore)

  case 'cache':
  case 'private-cache':
  case 'unstable-cache':
    // Inside of `"use cache"` or `unstable_cache`, draft mode is available if
    // the outmost work unit store is a request store (or a runtime prerender),
    // and if draft mode is enabled.
    const draftModeProvider = getDraftModeProviderForCacheScope(
      workStore,
      workUnitStore
    )

    if (draftModeProvider) {
      return createOrGetCachedDraftMode(draftModeProvider, workStore)
    }

  // Otherwise, we fall through to providing an empty draft mode.
  // eslint-disable-next-line no-fallthrough
  case 'prerender':
  case 'prerender-legacy':
    // Return empty draft mode
    return createOrGetCachedDraftMode(null, workStore)
  case 'prerender-client':
  case 'validation-client': {
    const exportName = '`draftMode`'
    throw new InvariantError(
      `${exportName} must not be used within a Client Component. Next.js should be preventing ${exportName} from being included in Client Components statically, but did not in this case.`
    )
  }
  case 'build-time-generator':
    throw new Error(
      `Route ${workStore.route} used \`${callingExpression}()\` inside \`${workUnitStore.functionName}\`. This is not supported because \`${workUnitStore.functionName}\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context`
    )

  default:
    return workUnitStore satisfies never
}
```
Sources: [packages/next/src/server/request/draft-mode.ts:40-92](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/draft-mode.ts#L40-L92)

| Work Unit Type | Action / Behavior on `draftMode()` Access |
| :--- | :--- |
| `prerender-runtime` | Delays until session data stage via `stagedRendering.delayUntilStage`, or retrieves cached draft mode. |
| `request` | Resolves or creates cached draft mode via `createOrGetCachedDraftMode`. |
| `cache`, `private-cache`, `unstable-cache` | Resolves provider via `getDraftModeProviderForCacheScope` if enclosed by a valid request store or runtime prerender. |
| `prerender`, `prerender-legacy` | Returns an empty draft mode instance. |
| `prerender-client`, `validation-client` | Throws an `InvariantError` indicating `draftMode` must not be used within Client Components. |
| `build-time-generator` | Throws an `Error` because build-time execution lacks an HTTP request context. |

Sources: [packages/next/src/server/request/draft-mode.ts:40-92](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/draft-mode.ts#L40-L92)

> [!CAUTION]
> Invoking `draftMode()` within Client Components (`prerender-client` or `validation-client`) immediately throws an `InvariantError`, as Next.js restricts draft mode hooks to server contexts.

Sources: [packages/next/src/server/request/draft-mode.ts:78-84](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/draft-mode.ts#L78-L84)

### Mutation Tracking and Bailout Handling

```typescript
function trackDynamicDraftMode(expression: string, constructorOpt: Function) {
  const workStore = workAsyncStorage.getStore()
  const workUnitStore = workUnitAsyncStorage.getStore()

  if (workStore) {
    // We have a store we want to track dynamic data access to ensure we
    // don't statically generate routes that manipulate draft mode.
    if (workUnitStore?.phase === 'after') {
      throw new Error(
        `Route ${workStore.route} used "${expression}" inside \`after()\`. The enabled status of \`draftMode()\` can be read inside \`after()\` but you cannot enable or disable \`draftMode()\`. See more info here: https://nextjs.org/docs/app/api-reference/functions/after`
      )
    }

    if (workStore.dynamicShouldError) {
      throw new StaticGenBailoutError(
        `Route ${workStore.route} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
      )
    }

    if (workUnitStore) {
      switch (workUnitStore.type) {
        case 'cache':
        case 'private-cache': {
          const error = createDraftModeMutationInUseCacheError(
            workStore.route,
            expression
          )
          Error.captureStackTrace(error, constructorOpt)
          applyOwnerStack(error)
          workStore.invalidDynamicUsageError ??= error
          throw error
        }
        case 'unstable-cache':
          throw createDraftModeMutationInUnstableCacheError(
            workStore.route,
            expression
          )

        case 'prerender':
        case 'prerender-runtime': {
          const error = new Error(
            `Route ${workStore.route} used ${expression} without first calling \`await connection()\`. See more info here: https://nextjs.org/docs/messages/next-prerender-sync-headers`
          )
          return abortAndThrowOnSynchronousRequestDataAccess(
            workStore.route,
            expression,
            error,
            workUnitStore
          )
        }
        case 'prerender-client':
        case 'validation-client':
          const exportName = '`draftMode`'
          throw new InvariantError(
            `${exportName} must not be used within a Client Component. Next.js should be preventing ${exportName} from being included in Client Components statically, but did not in this case.`
          )
        case 'prerender-legacy':
          workUnitStore.revalidate = 0

          const err = new DynamicServerError(
            `Route ${workStore.route} couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
          )
          workStore.dynamicUsageDescription = expression
          workStore.dynamicUsageStack = err.stack

          throw err
        case 'request':
          trackDynamicDataInDynamicRender(workUnitStore)
          break
        case 'build-time-generator':
          throw new Error(
            `Route ${workStore.route} used \`${expression}\` inside \`${workUnitStore.functionName}\`. This is not supported because \`${workUnitStore.functionName}\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context`
          )
        default:
          workUnitStore satisfies never
      }
    }
  }
```
Sources: [packages/next/src/server/request/draft-mode.ts:194-271](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/draft-mode.ts#L194-L271)

| Work Unit Store Type | Mutation Response (`trackDynamicDraftMode`) |
| :--- | :--- |
| `cache`, `private-cache` | Throws a mutation error created by `createDraftModeMutationInUseCacheError`. |
| `unstable-cache` | Throws a mutation error created by `createDraftModeMutationInUnstableCacheError`. |
| `prerender`, `prerender-runtime` | Aborts and throws due to synchronous request data access without `await connection()`. |
| `prerender-legacy` | Sets `revalidate = 0` and throws a `DynamicServerError`. |
| `request` | Tracks dynamic data access via `trackDynamicDataInDynamicRender`. |
| `build-time-generator` | Throws an error prohibiting runtime API use inside build-time generator functions. |

Sources: [packages/next/src/server/request/draft-mode.ts:213-267](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/draft-mode.ts#L213-L267)

> [!WARNING]
> Attempting to enable or disable draft mode inside an `after()` execution block triggers an error because draft mode status can only be read, not mutated, post-response.

Sources: [packages/next/src/server/request/draft-mode.ts:201-205](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request/draft-mode.ts#L201-L205)

## Vary Params Accumulation

### VaryParamsAccumulator Mechanics

```typescript
export class VaryParamsAccumulator implements AsyncIterable<VaryParamId> {
  private _resolve: ((result: IteratorResult<VaryParamId>) => void) | null =
    null
  private _done = false
  private _buffer: VaryParamId[] = []
  // The set of param names already yielded. Doubles as the dedupe guard so the
  // same name is never emitted twice.
  private _seen: Set<VaryParamId> = new Set()

  /**
   * Records that a param was accessed. Yields the name into the stream the
   * first time it's seen; subsequent accesses of the same name are no-ops.
   */
  add(id: VaryParamId): void {
    if (this._done || this._seen.has(id)) {
      return
    }
    this._seen.add(id)
    if (this._resolve !== null) {
      this._resolve({ value: id, done: false })
      this._resolve = null
    } else {
      this._buffer.push(id)
    }
  }

  /** Ends the iteration. Best-effort: if skipped (e.g. on a sync-I/O abort),
   * the consumer simply reads the params yielded so far. */
  close(): void {
    if (this._done) {
      return
    }
    this._done = true
    if (this._resolve !== null) {
      this._resolve({ value: undefined, done: true })
      this._resolve = null
    }
  }

  [Symbol.asyncIterator](): AsyncIterator<VaryParamId> {
    return {
      next: () => {
        if (this._buffer.length > 0) {
          return Promise.resolve({ value: this._buffer.shift()!, done: false })
        }
        if (this._done) {
          return Promise.resolve({ value: undefined, done: true })
        }
        return new Promise<IteratorResult<VaryParamId>>((resolve) => {
          this._resolve = resolve
        })
      },
    }
  }
}
```
Sources: [packages/next/src/server/app-render/vary-params.ts:36-90](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/vary-params.ts#L36-L90)

| Method / Property | Type / Signature | Behavior |
| :--- | :--- | :--- |
| `_resolve` | `((result: IteratorResult<VaryParamId>) => void) \| null` | Pending resolver for the async iterator. |
| `_done` | `boolean` | Tracks whether accumulation has been closed. |
| `_buffer` | `VaryParamId[]` | Stores parameter names yielded before the iterator pulls them. |
| `_seen` | `Set<VaryParamId>` | Deduplication set ensuring each parameter name is emitted at most once. |
| `add()` | `(id: VaryParamId) => void` | Records a parameter access, buffering or resolving immediately. |
| `close()` | `() => void` | Ends the async iterable and resolves any waiting iterator promises. |
| `[Symbol.asyncIterator]()` | `() => AsyncIterator<VaryParamId>` | Returns an async iterator consuming the buffer and future additions. |

Sources: [packages/next/src/server/app-render/vary-params.ts:36-90](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/vary-params.ts#L36-L90)

> [!NOTE]
> `VaryParamsAccumulator` acts as an `AsyncIterable<VaryParamId>` serialized directly by React Flight, streaming accessed parameters incrementally so that aborted prerenders capture exactly the parameters needed for the partial response.

Sources: [packages/next/src/server/app-render/vary-params.ts:13-26](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/vary-params.ts#L13-L26)

### Parameter Proxies and Response Accumulation

```typescript
export function createVaryingParams(
  accumulator: VaryParamsAccumulator,
  originalParamsObject: Params,
  optionalCatchAllParamName: string | null
): Params {
  if (optionalCatchAllParamName !== null) {
    // When there's an optional catch-all param with no value (e.g.,
    // [[...slug]] at /), the param doesn't exist as a property on the params
    // object. Use a Proxy to track all param access — both existing params
    // and the missing optional param — including enumeration patterns like
    // Object.keys(), spread, for...in, and `in` checks.
    return new Proxy(originalParamsObject, {
      get(target, prop, receiver) {
        if (typeof prop === 'string') {
          if (
            prop === optionalCatchAllParamName ||
            Object.prototype.hasOwnProperty.call(target, prop)
          ) {
            accumulateVaryParam(accumulator, prop)
          }
        }
        return Reflect.get(target, prop, receiver)
      },
      has(target, prop) {
        if (prop === optionalCatchAllParamName) {
          accumulateVaryParam(accumulator, optionalCatchAllParamName)
        }
        return Reflect.has(target, prop)
      },
      ownKeys(target) {
        // Enumerating the params object means the user's code may depend on
        // which params are present, so conservatively track the optional
        // param as accessed.
        accumulateVaryParam(accumulator, optionalCatchAllParamName)
        return Reflect.ownKeys(target)
      },
    })
  }

  // When there's no optional catch-all, all params exist as properties on the
  // object, so we can use defineProperty getters instead of a Proxy. This is
  // faster because the engine can optimize property access on regular objects
  // more aggressively than Proxy trap calls.
  const underlyingParamsWithVarying: Params = {}
  for (const paramName in originalParamsObject) {
    Object.defineProperty(underlyingParamsWithVarying, paramName, {
      get() {
        accumulateVaryParam(accumulator, paramName)
        return originalParamsObject[paramName]
      },
      enumerable: true,
    })
  }
  return underlyingParamsWithVarying
}

export function createVaryingSearchParams(
  accumulator: VaryParamsAccumulator,
  originalSearchParamsObject: SearchParams
): SearchParams {
  // Search params have no fixed schema, so any access — missing-key reads, `in`
  // checks, or enumeration — must register as varying. A Proxy is required
  // (rather than per-property getters) so that enumeration of an empty
  // searchParams object still triggers a vary. All accesses bucket into the
  // single search params id; the segment is keyed by the whole query string.
  // TODO: Split into per-param tracking if the cache key evolves.
  return new Proxy(originalSearchParamsObject, {
    get(target, prop, receiver) {
      if (typeof prop === 'string') {
        accumulateVaryParam(accumulator, SEARCH_PARAMS_VARY_ID)
      }
      return Reflect.get(target, prop, receiver)
    },
    has(target, prop) {
      if (typeof prop === 'string') {
        accumulateVaryParam(accumulator, SEARCH_PARAMS_VARY_ID)
      }
      return Reflect.has(target, prop)
    },
    ownKeys(target) {
```
Sources: [packages/next/src/server/app-render/vary-params.ts:205-289](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/vary-params.ts#L205-L289)

| Function | Parameters | Return Type | Description |
| :--- | :--- | :--- | :--- |
| `createResponseVaryParamsAccumulator` | None | `ResponseVaryParamsAccumulator` | Creates top-level `head`, `rootParams`, and a `segments` set. |
| `createVaryParamsAccumulator` | None | `VaryParamsAccumulator \| null` | Allocates and registers a new segment accumulator in the current work unit. |
| `createVaryingParams` | `accumulator, originalParamsObject, optionalCatchAllParamName` | `Params` | Returns a Proxy for optional catch-alls or `defineProperty` getters for standard params. |
| `createVaryingSearchParams` | `accumulator, originalSearchParamsObject` | `SearchParams` | Wraps search parameters in a Proxy recording `SEARCH_PARAMS_VARY_ID`. |
| `finishAccumulatingVaryParams` | `responseAccumulator` | `void` | Closes `head`, `rootParams`, and all route segment accumulators. |

Sources: [packages/next/src/server/app-render/vary-params.ts:123-157](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/vary-params.ts#L123-L157), [packages/next/src/server/app-render/vary-params.ts:205-289](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/vary-params.ts#L205-L289), [packages/next/src/server/app-render/vary-params.ts:305-313](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/vary-params.ts#L305-L313)

> [!TIP]
> When no optional catch-all parameter exists, `createVaryingParams` uses `Object.defineProperty` property getters rather than a Proxy, allowing JavaScript engines to optimize property access more aggressively.

Sources: [packages/next/src/server/app-render/vary-params.ts:244-258](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/vary-params.ts#L244-L258)

## Postponed State Serialization

### State Layout and Serialization Workflow

```mermaid
flowchart TD
    A["Dynamic State"] --> B{"Has Fallback Params?"}
    B -- Yes --> C["Serialize Fallback Params JSON"]
    C --> D["Construct postponedString: prefix with param length"]
    B -- No --> E["Construct postponedString: raw dataString"]
    D --> F["serializePostponedState"]
    E --> F
    F --> G["stringifyResumeDataCache"]
    G --> H{"Compression Enabled?"}
    H -- Yes --> I["deflateResumeDataCache (zlib base64)"]
    H -- No --> J["Keep Raw JSON String"]
    I --> K["Concatenate: length prefix and cache payload"]
    J --> K
```
Sources: [packages/next/src/server/app-render/postponed-state.ts:88-164](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/postponed-state.ts#L88-L164)

| Function | Parameters | Return Type | Description |
| :--- | :--- | :--- | :--- |
| `serializePostponedState` | `postponedString, resumeDataCache, isCacheComponentsEnabled, maxPostponedStateSizeBytes, disableResumeDataCacheCompression` | `Promise<string>` | Prefixes postponed payload with length, serializes and optionally compresses resume data cache via zlib. |
| `getDynamicHTMLPostponedState` | `postponed, preludeState, fallbackRouteParams, resumeDataCache, isCacheComponentsEnabled, maxPostponedStateSizeBytes, disableResumeDataCacheCompression` | `Promise<string>` | Encodes React postponed state, prelude marker state, and fallback route parameters into a string. |
| `getDynamicDataPostponedState` | `resumeDataCache, isCacheComponentsEnabled, maxPostponedStateSizeBytes, disableResumeDataCacheCompression, fallbackRouteParams` | `Promise<string>` | Generates postponed state for dynamic data access without React HTML-resume requirements. |
| `parsePostponedState` | `state, maxPostponedStateSizeBytes, disableResumeDataCacheCompression` | `PostponedState` | Parses serialized postponed string back into `DynamicDataPostponedState` or `DynamicHTMLPostponedState`. |
| `parseResumeDataCacheFromPostponedState` | `state, maxPostponedStateSizeBytes, disableResumeDataCacheCompression` | `RenderResumeDataCache` | Extracts and parses the `RenderResumeDataCache` portion from a postponed state string. |
| `isEmptyHTMLPrelude` | `state` | `boolean` | Quickly checks whether the static shell produced zero bytes before encountering a dynamic hole. |

Sources: [packages/next/src/server/app-render/postponed-state.ts:88-192](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/postponed-state.ts#L88-L192), [packages/next/src/server/app-render/postponed-state.ts:225-327](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/postponed-state.ts#L225-L327), [packages/next/src/server/app-render/postponed-state.ts:392-428](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/postponed-state.ts#L392-L428)

> [!NOTE]
> Next.js restricts runtime execution environment context by enforcing that `stringifyResumeDataCache`, `deflateResumeDataCache`, and `createRenderResumeDataCache` throw an `InvariantError` if invoked inside the `edge` runtime.

Sources: [packages/next/src/server/resume-data-cache/resume-data-cache.ts:145-149](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/resume-data-cache/resume-data-cache.ts#L145-L149), [packages/next/src/server/resume-data-cache/resume-data-cache.ts:181-185](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/resume-data-cache/resume-data-cache.ts#L181-L185), [packages/next/src/server/resume-data-cache/resume-data-cache.ts:253-257](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/resume-data-cache/resume-data-cache.ts#L253-L257)

### Resume Data Cache Structures and Decompression Limits

```typescript
export interface RenderResumeDataCache {
  /**
   * Discriminator. `false` means this cache is read-only and cannot be filled
   * with new entries. Used by `ResumeDataCache` consumers to narrow the union
   * via standard discriminated-union narrowing (e.g. `if
   * (resumeDataCache.mutable)`).
   */
  readonly mutable: false

  /**
   * A read-only Map store for values cached by the 'use cache' React hook.
   * The 'set' operation is omitted to enforce immutability.
   */
  readonly cache: Omit<UseCacheCacheStore, 'set'>

  /**
   * A read-only Map store for cached fetch responses.
   * The 'set' operation is omitted to enforce immutability.
   */
  readonly fetch: Omit<FetchCacheStore, 'set'>

  /**
   * A read-only Map store for encrypted bound args of inline server functions.
   * The 'set' operation is omitted to enforce immutability.
   */
  readonly encryptedBoundArgs: Omit<EncryptedBoundArgsCacheStore, 'set'>

  /**
   * A read-only Map store for decrypted bound args of inline server functions.
   * This is only intended for in-memory usage during pre-rendering, and must
   * not be persisted in the resume store. The 'set' operation is omitted to
   * enforce immutability.
   */
  readonly decryptedBoundArgs: Omit<DecryptedBoundArgsCacheStore, 'set'>

  /**
   * A read-only in-memory Map store for rendered `ImageResponse` array buffers.
   * This is only intended for in-memory usage during pre-rendering, and must
   * not be persisted in the resume store. The 'set' operation is omitted to
   * enforce immutability.
   */
  readonly imageResponses: Omit<ImageResponseCacheStore, 'set'>
}

/**
 * A mutable version of the resume data cache used during pre-rendering.
 * This cache allows both reading and writing of cached values.
 */
export interface PrerenderResumeDataCache {
  /**
   * Discriminator. `true` means this cache is mutable and can be filled with
   * new entries during this prerender. Used by `ResumeDataCache` consumers to
   * narrow the union via standard discriminated-union narrowing (e.g.
   * `if (resumeDataCache.mutable)`).
   */
  readonly mutable: true

  /**
   * A mutable Map store for values cached by the 'use cache' React hook.
   * Supports both 'get' and 'set' operations to build the cache during
   * pre-rendering.
   */
  readonly cache: UseCacheCacheStore

  /**
   * A mutable Map store for cached fetch responses.
   * Supports both 'get' and 'set' operations to build the cache during
   * pre-rendering.
   */
  readonly fetch: FetchCacheStore

  /**
   * A mutable Map store for encrypted bound args of inline server functions.
   * Supports both 'get' and 'set' operations to build the cache during
   * pre-rendering.
   */
  readonly encryptedBoundArgs: EncryptedBoundArgsCacheStore

  /**
   * A mutable Map store for decrypted bound args of inline server functions.
```
Sources: [packages/next/src/server/resume-data-cache/resume-data-cache.ts:17-116](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/resume-data-cache/resume-data-cache.ts#L17-L116)

> [!WARNING]
> When decompressing a persisted resume data cache using `createRenderResumeDataCache`, the maximum decompressed size defaults to `500 * 1024 * 1024` bytes (or 5 times `maxPostponedStateSizeBytes` if configured), throwing an `ERR_BUFFER_TOO_LARGE` error if breached to protect against zip-bomb payloads.

Sources: [packages/next/src/server/resume-data-cache/resume-data-cache.ts:287-312](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/resume-data-cache/resume-data-cache.ts#L287-L312)

## Runtime Postponed Execution

### Shell Streaming and Resume Orchestration

```mermaid
flowchart TD
    A["Incoming Request"] --> B{"Has Postponed State?"}
    B -- Yes --> C["Read Body with Size Limit"]
    C --> D{"Size Exceeded?"}
    D -- Yes --> E["Return 413 Payload Too Large"]
    D -- No --> F["Decode UTF-8 Postponed State"]
    B -- No --> G["Evaluate PPR Route & Query"]
    F --> H["Set Request Meta 'postponed'"]
    H --> I["Determine isRoutePPREnabled"]
    G --> I
    I --> J{"Streaming Metadata Supported?"}
    J -- No --> K["Force Dynamic PPR Render"]
    J -- Yes --> L["Proceed with PPR Execution"]
```
Sources: [packages/next/src/build/templates/app-page-runtime.ts:425-441](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/app-page-runtime.ts#L425-L441), [packages/next/src/build/templates/app-page-runtime.ts:481-494](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/app-page-runtime.ts#L481-L494), [packages/next/src/build/templates/app-page-runtime.ts:561-570](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/app-page-runtime.ts#L561-L570)

When an application page handles a request, the runtime inspects the incoming body for serialized postponed state when running in minimal mode or resuming execution. The `maxPostponedStateSize` configuration governs how large the incoming suspended state payload can grow before the server rejects it. If the buffer read via `readBodyWithSizeLimit` returns `null`, the request aborts immediately with a `413` status code, emitting an error message derived from `getPostponedStateExceededErrorMessage`.

Sources: [packages/next/src/build/templates/app-page-runtime.ts:425-441](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/app-page-runtime.ts#L425-L441)

> [!WARNING]
> A Partially Static (PPR) shell closes its document head before any dynamic render resumes. If the user agent is identified as an HTML-limited bot via `shouldServeStreamingMetadata`, blocking metadata resolved during resume would be emitted after the head. In response, Next.js forces a dynamic PPR render by setting `shouldForceDynamicPPRRender` to `true`, ensuring blocking metadata remains in the initial document head.

Sources: [packages/next/src/build/templates/app-page-runtime.ts:561-570](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/app-page-runtime.ts#L561-L570)

### Testing APIs and Client Cache Interaction

In addition to serving standard page responses, the runtime integrates with the [Segment Cache](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/client-runtime/segment-cache) scheduler to serve prefetched static segments while dynamically evaluating missed branches on navigation.

```typescript
const hasDebugStaticShellQuery =
  process.env.__NEXT_EXPERIMENTAL_STATIC_SHELL_DEBUGGING === '1' &&
  typeof query.__nextppronly !== 'undefined' &&
  couldSupportPPR

// When enabled, this will allow the use of the `?__nextppronly` query
// to enable debugging of the fallback shell.
const hasDebugFallbackShellQuery =
  hasDebugStaticShellQuery && query.__nextppronly === 'fallback'

// Whether the testing API is exposed (dev mode or explicit flag)
const exposeTestingApi =
  nextConfig.cacheComponents === true &&
  (routeModule.isDev === true ||
    nextConfig.experimental.exposeTestingApiInProductionBuild === true)
```
Sources: [packages/next/src/build/templates/app-page-runtime.ts:445-460](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/app-page-runtime.ts#L445-L460)

The runtime uses `NEXT_INSTANT_TEST_COOKIE` in combination with navigation locks to evaluate whether a request represents an instant navigation test, isolating shell prefetches from runtime streaming content. On the client, `spawnDynamicRequests` and `writeDynamicDataIntoNavigationTask` parse the incoming Flight payload and update cache nodes, marking tasks as fulfilled or triggering lazy fetches when segments require additional dynamic resolution.

Sources: [packages/next/src/build/templates/app-page-runtime.ts:472-478](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/app-page-runtime.ts#L472-L478), [packages/next/src/client/components/render-tree.ts:1328-1360](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/components/render-tree.ts#L1328-L1360), [packages/next/src/client/components/render-tree.ts:1895-1922](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/components/render-tree.ts#L1895-L1922)

## Related

- [Incremental Cache](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/incremental-cache)
- [App Rendering](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/app-rendering)
- [Async Storage](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/async-storage)


## Sitemap

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