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

Edge Runtime

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

The Edge Runtime powers Next.js middleware, edge API routes, and edge-rendered App and Pages router components by executing userland code in a lightweight, web-standard V8 sandbox. Built on top of the edge-runtime package, the edge architecture establishes a secure global environment with process polyfills, stubs for unsupported Node.js APIs, and specialized Webpack loaders that package entrypoints and dependency manifests. Incoming HTTP requests and asynchronous fetch event lifecycles are normalized across the adapter layer, while bridge wrappers connect edge route modules to execution handlers, streaming responses, and waitUntil background task primitives.

Sources: packages/next/src/server/web/sandbox/sandbox.ts:1-44, packages/next/src/server/web/sandbox/context.ts:259-282, packages/next/src/build/webpack/plugins/middleware-plugin.ts:44-74, packages/next/src/server/web/adapter.ts:259-278, packages/next/src/server/web/edge-route-module-wrapper.ts:35-81

Edge Compilation and Manifest Packaging

Webpack Loaders for Edge Entrypoints

Next.js orchestrates the compilation and packaging of Edge Runtime components through specialized Webpack loaders and the MiddlewarePlugin. These components transform userland modules, collect module build metadata, manage asset and WebAssembly bindings, and emit the middleware.json manifest version 3.

Edge compilation relies on dedicated loaders that attach build information to webpack modules via getModuleBuildInfo. The EdgeAppRouteLoader processes App Router routes and page paths, configuring cache handlers and route metadata. Similarly, edgeSSRLoader handles Pages and App router Server-Side Rendering (SSR) entrypoints, swapping standard folder paths for their ESM equivalents (next/dist/esm/pages) to optimize tree-shaking and bundle sizing.

Loader NameSource FileKey Responsibilities
EdgeAppRouteLoaderpackages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.tsConfigures nextEdgeSSR build info, parses base64 middleware configurations, and sets up cache handlers for App Router route modules.
edgeSSRLoaderpackages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.tsResolves absolute paths for documents, error boundaries, and 500 pages while mapping dist folders to ESM variants.
nextEdgeFunctionLoaderpackages/next/src/build/webpack/loaders/next-edge-function-loader.tsInjects global web polyfills, wraps page handlers using wrapApiHandler, and exports an adapter instance for Edge API routes.
MiddlewareWasmLoaderpackages/next/src/build/webpack/loaders/next-middleware-wasm-loader.tsComputes a SHA-1 hash of binary WebAssembly files, emits chunks into server/edge-chunks/, and exposes module bindings.
MiddlewareAssetLoaderpackages/next/src/build/webpack/loaders/next-middleware-asset-loader.tsInterpolates asset names with hashes, emits files to server/edge-chunks/, and exports a blob URL identifier string.

Sources: packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts:57-117, packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts:104-228, packages/next/src/build/webpack/loaders/next-edge-function-loader.ts:15-71, packages/next/src/build/webpack/loaders/next-middleware-wasm-loader.ts:8-15, packages/next/src/build/webpack/loaders/next-middleware-asset-loader.ts:4-17

export default function MiddlewareWasmLoader(this: any, source: Buffer) {
  const name = `wasm_${sha1(source)}`
  const filePath = `edge-chunks/${name}.wasm`
  const buildInfo = getModuleBuildInfo(this._module)
  buildInfo.nextWasmMiddlewareBinding = { filePath: `server/${filePath}`, name }
  this.emitFile(`/${filePath}`, source, null)
  return `module.exports = ${name};`
}

Sources: packages/next/src/build/webpack/loaders/next-middleware-wasm-loader.ts:8-15

MiddlewarePlugin Manifest Generation

The MiddlewarePlugin compiles execution metadata across all entrypoints to construct version 3 of the MiddlewareManifest. This manifest tracks sorted middleware patterns alongside defined middleware functions and edge SSR entrypoints, mapping each entrypoint to its generated file dependencies, environment variables, WebAssembly bindings, asset bindings, and target execution regions.

export interface EdgeFunctionDefinition {
  files: string[]
  name: string
  page: string
  entrypoint: string
  matchers: ProxyMatcher[]
  env: Record<string, string>
  wasm?: AssetBinding[]
  assets?: AssetBinding[]
  regions?: string[] | string
}
 
export interface MiddlewareManifest {
  version: 3
  sortedMiddleware: string[]
  middleware: { [page: string]: EdgeFunctionDefinition }
  functions: { [page: string]: EdgeFunctionDefinition }
}

Sources: packages/next/src/build/webpack/plugins/middleware-plugin.ts:44-64

Note

MiddlewarePlugin extracts file dependencies using getEntryFiles, which differentiates between server components requiring client reference manifests and standard edge functions, ensuring that Subresource Integrity (SRI) and dynamic CSS manifests are correctly bundled for edge SSR targets.

Sources: packages/next/src/build/webpack/plugins/middleware-plugin.ts:105-158

Global Environment and Runtime Polyfills

Environment Bootstrapping and Global Enhancement

When the runtime initialization checks process.env.NEXT_RUNTIME === 'edge', enhanceGlobals sets up the global environment. It synchronizes process.env with global.process.env and overwrites global.process to point to the sandboxed process instance. It also defines __import_unsupported on globalThis as a non-enumerable, non-configurable property. This helper returns a proxy that throws an error when property access, construction, or function invocation occurs on un-supported Node.js modules, enabling codebases to import modules that are unused at runtime without failing build checks. Additionally, it eagerly triggers ensureInstrumentationRegistered to initialize telemetry or instrumentation hooks.

function getUnsupportedModuleErrorMessage(module: string) {
  // warning: if you change these messages, you must adjust how dev-overlay's middleware detects modules not found
  return `The edge runtime does not support Node.js '${module}' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`
}
 
function __import_unsupported(moduleName: string) {
  const proxy: any = new Proxy(function () {}, {
    get(_obj, prop) {
      if (prop === 'then') {
        return {}
      }
      throw new Error(getUnsupportedModuleErrorMessage(moduleName))
    },
    construct() {
      throw new Error(getUnsupportedModuleErrorMessage(moduleName))
    },
    apply(_target, _this, args) {
      if (typeof args[0] === 'function') {
        return args[0](proxy)
      }
      throw new Error(getUnsupportedModuleErrorMessage(moduleName))
    },
  })
  return new Proxy({}, { get: () => proxy })
}
 
function enhanceGlobals() {
  if (process.env.NEXT_RUNTIME !== 'edge') {
    return
  }
 
  // The condition is true when the "process" module is provided
  if (process !== global.process) {
    // prefer local process but global.process has correct "env"
    process.env = global.process.env
    global.process = process
  }
 
  // to allow building code that import but does not use node.js modules,
  // webpack will expect this function to exist in global scope
  try {
    Object.defineProperty(globalThis, '__import_unsupported', {
      value: __import_unsupported,
      enumerable: false,
      configurable: false,
    })
  } catch {}
 
  // Eagerly fire instrumentation hook to make the startup faster.
  void ensureInstrumentationRegistered()
}

Sources: packages/next/src/server/web/globals.ts:57-108

Warning

Accessing any property or calling a function on an unsupported Node.js module proxy created by __import_unsupported immediately throws an error stating that the edge runtime does not support that module.

Sources: packages/next/src/server/web/globals.ts:57-82

Runtime Context Extension and Node API Stubs

The module context creation function initializes an EdgeRuntime instance with code generation flags enabled when process.env.NODE_ENV !== 'production'. Through its extend hook, it attaches process polyfills, custom require implementations via NativeModuleMap, timers (setInterval, clearInterval, setTimeout, clearTimeout), performance hooks, and AsyncLocalStorage. It iterates over EDGE_UNSUPPORTED_NODE_APIS to add stubs for missing Node modules and attaches custom handlers for dynamic code evaluation warnings.

  const runtime = new EdgeRuntime({
    codeGeneration:
      process.env.NODE_ENV !== 'production'
        ? { strings: true, wasm: true }
        : undefined,
    extend: (context) => {
      context.process = createProcessPolyfill(edgeFunctionEntry.env)
 
      Object.defineProperty(context, 'require', {
        enumerable: false,
        value: (id: string) => {
          const value = NativeModuleMap.get(id)
          if (!value) {
            throw TypeError('Native module not found: ' + id)
          }
          return value
        },
      })
 
      if (process.env.NODE_ENV !== 'production') {
        context.__next_log_error__ = function (err: unknown) {
          options.onError(err)
        }
      }
 
      context.__next_eval__ = function __next_eval__(fn: Function) {
        const key = fn.toString()
        if (!warnedEvals.has(key)) {
          const warning = getServerError(
            new Error(
              `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`
            ),
            COMPILER_NAMES.edgeServer
          )
          warning.name = 'DynamicCodeEvaluationWarning'
          Error.captureStackTrace(warning, __next_eval__)
          warnedEvals.add(key)
          options.onWarning(warning)
        }
        return fn()
      }
 
      context.__next_webassembly_compile__ =
        function __next_webassembly_compile__(fn: Function) {
          const key = fn.toString()
          if (!warnedWasmCodegens.has(key)) {
            const warning = getServerError(
              new Error(`Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Edge Runtime.
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`),
              COMPILER_NAMES.edgeServer
            )
            warning.name = 'DynamicWasmCodeGenerationWarning'
            Error.captureStackTrace(warning, __next_webassembly_compile__)
            warnedWasmCodegens.add(key)
            options.onWarning(warning)
          }
          return fn()
        }
 
      context.__next_webassembly_instantiate__ =
        async function __next_webassembly_instantiate__(fn: Function) {
          const result = await fn()
 
          // If a buffer is given, WebAssembly.instantiate returns an object
          // containing both a module and an instance while it returns only an
          // instance if a WASM module is given. Utilize the fact to determine
          // if the WASM code generation happens.
          //
          // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate#primary_overload_%E2%80%94_taking_wasm_binary_code
          const instantiatedFromBuffer = result.hasOwnProperty('module')
 
          const key = fn.toString()
          if (instantiatedFromBuffer && !warnedWasmCodegens.has(key)) {
            const warning = getServerError(
              new Error(`Dynamic WASM code generation ('WebAssembly.instantiate' with a buffer parameter) not allowed in Edge Runtime.
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`),
              COMPILER_NAMES.edgeServer
            )
            warning.name = 'DynamicWasmCodeGenerationWarning'

Sources: packages/next/src/server/web/sandbox/context.ts:264-464

Note

Invoking eval or new Function triggers __next_eval__, which checks a warnedEvals set to emit a DynamicCodeEvaluationWarning once per unique function string in non-production environments.

Sources: packages/next/src/server/web/sandbox/context.ts:289-305

Development Sandbox Execution and Isolation

Module Context Creation and Global Injection

The createModuleContext function constructs an isolated EdgeRuntime environment for local development. It loads WebAssembly binaries from the edge function entry and configures code generation flags based on environment conditions. Within the runtime extension hook, it provisions custom globals including process polyfills, native module resolution via NativeModuleMap, timers, performance monitoring, and an overridden fetch implementation capable of resolving inline assets. It also intercepts Request constructor calls and Response.redirect to validate target URLs.

async function createModuleContext(options: ModuleContextOptions) {
  const warnedEvals = new Set<string>()
  const warnedWasmCodegens = new Set<string>()
  const { edgeFunctionEntry } = options
  const wasm = await loadWasm(edgeFunctionEntry.wasm ?? [])
  const runtime = new EdgeRuntime({
    codeGeneration:
      process.env.NODE_ENV !== 'production'
        ? { strings: true, wasm: true }
        : undefined,
    extend: (context) => {
      context.process = createProcessPolyfill(edgeFunctionEntry.env)
 
      Object.defineProperty(context, 'require', {
        enumerable: false,
        value: (id: string) => {
          const value = NativeModuleMap.get(id)
          if (!value) {
            throw TypeError('Native module not found: ' + id)
          }
          return value
        },
      })
 
      if (process.env.NODE_ENV !== 'production') {
        context.__next_log_error__ = function (err: unknown) {
          options.onError(err)
        }
      }
 
      context.__next_eval__ = function __next_eval__(fn: Function) {
        const key = fn.toString()
        if (!warnedEvals.has(key)) {
          const warning = getServerError(
            new Error(
              `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`
            ),
            COMPILER_NAMES.edgeServer
          )
          warning.name = 'DynamicCodeEvaluationWarning'
          Error.captureStackTrace(warning, __next_eval__)
          warnedEvals.add(key)
          options.onWarning(warning)
        }
        return fn()
      }
 
      context.__next_webassembly_compile__ =
        function __next_webassembly_compile__(fn: Function) {
          const key = fn.toString()
          if (!warnedWasmCodegens.has(key)) {
            const warning = getServerError(
              new Error(`Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Edge Runtime.
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`),
              COMPILER_NAMES.edgeServer
            )
            warning.name = 'DynamicWasmCodeGenerationWarning'
            Error.captureStackTrace(warning, __next_webassembly_compile__)
            warnedWasmCodegens.add(key)
            options.onWarning(warning)
          }
          return fn()
        }
 
      context.__next_webassembly_instantiate__ =
        async function __next_webassembly_instantiate__(fn: Function) {
          const result = await fn()
 
          // If a buffer is given, WebAssembly.instantiate returns an object
          // containing both a module and an instance while it returns only an
          // instance if a WASM module is given. Utilize the fact to determine
          // if the WASM code generation happens.
          //
          // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate#primary_overload_%E2%80%94_taking_wasm_binary_code
          const instantiatedFromBuffer = result.hasOwnProperty('module')
 
          const key = fn.toString()
          if (instantiatedFromBuffer && !warnedWasmCodegens.has(key)) {
            const warning = getServerError(

Sources: packages/next/src/server/web/sandbox/context.ts:259-399

Warning

Attempting to instantiate WebAssembly modules from a buffer via WebAssembly.instantiate triggers __next_webassembly_instantiate__, emitting a DynamicWasmCodeGenerationWarning when detected outside precompiled modules.

Sources: packages/next/src/server/web/sandbox/context.ts:324-349

Runtime Context Evaluation and Runner Execution

The getRuntimeContext function initializes module contexts and injects shared global singletons into the sandbox globalThis object. These injections support incremental caching, router server contexts, server component HMR caching, and client asset tokens. Once injected, it evaluates all provided path entries within the context.

export async function getRuntimeContext(
  params: Omit<RunnerFnParams, 'request'>
): Promise<EdgeRuntime<any>> {
  const { runtime, evaluateInContext } = await getModuleContext({
    moduleName: params.name,
    onWarning: params.onWarning ?? (() => {}),
    onError: params.onError ?? (() => {}),
    useCache: params.useCache !== false,
    edgeFunctionEntry: params.edgeFunctionEntry,
    distDir: params.distDir,
  })
 
  if (params.incrementalCache) {
    runtime.context.globalThis.__incrementalCacheShared = true
    runtime.context.globalThis.__incrementalCache = params.incrementalCache
  }
 
  // expose router server context for access to dev handlers like
  // logErrorWithOriginalStack
  ;(runtime.context.globalThis as any as typeof routerServerGlobal)[
    RouterServerContextSymbol
  ] = routerServerGlobal[RouterServerContextSymbol]
 
  if (params.serverComponentsHmrCache) {
    runtime.context.globalThis.__serverComponentsHmrCache =
      params.serverComponentsHmrCache
  }
 
  if (params.clientAssetToken) {
    runtime.context.globalThis.NEXT_CLIENT_ASSET_SUFFIX =
      params.clientAssetToken ? `?dpl=${params.clientAssetToken}` : ''
  }
 
  for (const paramPath of params.paths) {
    evaluateInContext(paramPath)
  }
  return runtime
}

Sources: packages/next/src/server/web/sandbox/sandbox.ts:72-109

The run function executes the compiled edge function handler within wrapped AsyncLocalStorage request stores and custom request contexts. It clones request bodies for non-GET/HEAD methods, builds standard headers while filtering forbidden headers, and finalizes request body streams upon completion.

export const run = withTaggedErrors(async function runWithTaggedErrors(params) {
  const runtime = await getRuntimeContext(params)
 
  const edgeFunction: EdgeHandler = (
    await runtime.context._ENTRIES[`middleware_${params.name}`]
  ).default
 
  const cloned = !['HEAD', 'GET'].includes(params.request.method)
    ? params.request.body?.cloneBodyStream()
    : undefined
 
  const KUint8Array = runtime.evaluate('Uint8Array')
  const urlInstance = new URL(params.request.url)
 
  params.request.url = urlInstance.toString()
 
  const headers = new Headers()
  for (const [key, value] of Object.entries(params.request.headers)) {
    headers.set(key, value?.toString() ?? '')
  }
 
  try {
    let result: FetchEventResult | undefined = undefined
    const builtinRequestCtx: BuiltinRequestContextValue = {
      ...getBuiltinRequestContext(),
      // FIXME(after):
      // arguably, this is an abuse of "@next/request-context" --
      // it'd make more sense to simply forward its existing value into the sandbox (in `createModuleContext`)
      // but here we're using it to just pass in `waitUntil` regardless if we were running in this context or not.
      waitUntil: params.request.waitUntil,
    }
    await edgeSandboxNextRequestContext.run(builtinRequestCtx, () =>
      requestStore.run({ headers }, async () => {
        result = await edgeFunction({
          request: {
            ...params.request,
            body:
              cloned &&
              requestToBodyStream(runtime.context, KUint8Array, cloned),
          },
        })
        for (const headerName of FORBIDDEN_HEADERS) {
          result.response.headers.delete(headerName)
        }
      })
    )
 
    if (!result) throw new Error('Edge function did not return a response')
    return result
  } finally {
    await params.request.body?.finalize()
  }
})

Sources: packages/next/src/server/web/sandbox/sandbox.ts:111-163

Note

FORBIDDEN_HEADERS containing content-length, content-encoding, and transfer-encoding are unconditionally deleted from the resulting response headers before dispatch.

Sources: packages/next/src/server/web/sandbox/sandbox.ts:23-27, packages/next/src/server/web/sandbox/sandbox.ts:152-154

Error Tagging and Unhandled Event Handling

In development mode, withTaggedErrors decorates runner executions by passing caught errors and unhandled waitUntil rejections through getServerError with an 'edge-server' compiler tag, enabling original stack frames to be resolved and displayed correctly in development overlays.

function withTaggedErrors(fn: RunnerFn): RunnerFn {
  if (process.env.NODE_ENV === 'development') {
    const { getServerError } =
      require('../../dev/node-stack-frames') as typeof import('../../dev/node-stack-frames')
 
    return (params) =>
      fn(params)
        .then((result) => ({
          ...result,
          waitUntil: result?.waitUntil?.catch((error) => {
            // TODO: used COMPILER_NAMES.edgeServer instead. Verify that it does not increase the runtime size.
            throw getServerError(error, 'edge-server')
          }),
        }))
        .catch((error) => {
          // TODO: used COMPILER_NAMES.edgeServer instead
          throw getServerError(error, 'edge-server')
        })
  }
 
  return fn
}

Sources: packages/next/src/server/web/sandbox/sandbox.ts:49-70

Additionally, the runtime context binds event listeners for unhandled errors and rejections, using getDecorateUnhandledError and getDecorateUnhandledRejection to ensure runtime exceptions bubble up safely into the parent Node.js process with proper stack decorations.

const decorateUnhandledError = getDecorateUnhandledError(runtime)
runtime.context.addEventListener('error', decorateUnhandledError)
const decorateUnhandledRejection = getDecorateUnhandledRejection(runtime)
runtime.context.addEventListener(
  'unhandledrejection',
  decorateUnhandledRejection
)
 

Sources: packages/next/src/server/web/sandbox/context.ts:467-474

Edge Web Adapter Architecture

Request and Response Extensions

The adapter architecture bridges incoming Node.js requests and standard Web APIs through NextRequest and NextResponse, which extend the standard Request and Response interfaces with specialized Next.js behaviors. NextRequest parses incoming headers into structured RequestCookies via its internal symbol store and constructs a NextURL instance that handles internationalization, base paths, and query string normalization. When bodies are present in non-edge runtimes, NextRequest automatically sets the request duplex mode to 'half'.

export class NextRequest extends Request {
  /** @internal */
  [INTERNALS]: {
    cookies: RequestCookies
    url: string
    nextUrl: NextURL
  }
 
  constructor(input: URL | RequestInfo, init: RequestInit = {}) {
    const url =
      typeof input !== 'string' && 'url' in input ? input.url : String(input)
 
    validateURL(url)
 
    // node Request instance requires duplex option when a body
    // is present or it errors, we don't handle this for
    // Request being passed in since it would have already
    // errored if this wasn't configured
    if (process.env.NEXT_RUNTIME !== 'edge') {
      if (init.body && init.duplex !== 'half') {
        init.duplex = 'half'
      }
    }
 
    if (input instanceof Request) super(input, init)
    else super(url, init)
 
    const nextUrl = new NextURL(url, {
      headers: toNodeOutgoingHttpHeaders(this.headers),
      nextConfig: init.nextConfig,
    })
    this[INTERNALS] = {
      cookies: new RequestCookies(this.headers),
      nextUrl,
      url: process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE
        ? url
        : nextUrl.toString(),
    }
  }

Sources: packages/next/src/server/web/spec-extension/request.ts:14-52

NextResponse wraps response cookies with a proxy that intercepts .set() and .delete() calls to automatically serialize cookie values and synchronize them into the 'x-middleware-set-cookie' header. It also exposes static helper methods for creating specialized responses, such as NextResponse.json, NextResponse.redirect, NextResponse.rewrite, and NextResponse.next.

Static MethodDefault Status / HeaderDescription
NextResponse.jsonStandard JSON initInstantiates a response wrapping a JSON body
NextResponse.redirectStatus 307 (validates 301, 302, 303, 307, 308)Sets the Location header to the validated target URL
NextResponse.rewriteHeader x-middleware-rewriteSets the rewrite destination and handles middleware request overrides
NextResponse.nextHeader x-middleware-next: '1'Instructs the router to continue to the next middleware or route
export class NextResponse<Body = unknown> extends Response {
  [INTERNALS]: {
    cookies: ResponseCookies
    url?: NextURL
    body?: Body
  }
 
  constructor(body?: BodyInit | null, init: ResponseInit = {}) {
    super(body, init)
 
    const headers = this.headers
    const cookies = new ResponseCookies(headers)
 
    const cookiesProxy = new Proxy(cookies, {
      get(target, prop, receiver) {
        switch (prop) {
          case 'delete':
          case 'set': {
            return (...args: [string, string]) => {
              const result = Reflect.apply(target[prop], target, args)
              const newHeaders = new Headers(headers)
 
              if (result instanceof ResponseCookies) {
                headers.set(
                  'x-middleware-set-cookie',
                  result
                    .getAll()
                    .map((cookie) => stringifyCookie(cookie))
                    .join(',')
                )
              }
 
              handleMiddlewareField(init, newHeaders)
              return result
            }
          }
          default:
            return ReflectAdapter.get(target, prop, receiver)
        }
      },
    })
 
    this[INTERNALS] = {
      cookies: cookiesProxy,
      url: init.url
        ? new NextURL(init.url, {
            headers: toNodeOutgoingHttpHeaders(headers),
            nextConfig: init.nextConfig,
          })
        : undefined,
    }
  }
 
  [Symbol.for('edge-runtime.inspect.custom')]() {
    return {
      cookies: this.cookies,
      url: this.url,
      // rest of props come from Response
      body: this.body,
      bodyUsed: this.bodyUsed,
      headers: Object.fromEntries(this.headers),
      ok: this.ok,
      redirected: this.redirected,
      status: this.status,
      statusText: this.statusText,
      type: this.type,
    }
  }
 
  public get cookies() {
    return this[INTERNALS].cookies
  }
 
  static json<JsonBody>(
    body: JsonBody,
    init?: ResponseInit
  ): NextResponse<JsonBody> {
    const response: Response = Response.json(body, init)
    return new NextResponse(response.body, response)
  }

Sources: packages/next/src/server/web/spec-extension/response.ts:36-153

Warning

Passing an invalid HTTP status code to NextResponse.redirect that is absent from the REDIRECTS set (301, 302, 303, 307, 308) throws a synchronous RangeError.

Sources: packages/next/src/server/web/spec-extension/response.ts:10-10, packages/next/src/server/web/spec-extension/response.ts:118-123

FetchEvent and WaitUntil Lifecycle

The adapter manages background task continuation using NextFetchEvent and internal waitUntil tracking. The internal_runWithWaitUntil function intercepts asynchronous promises passed to the runtime, incrementing a global waitUntilCounter and provisioning a shared waitUntilPromise that only resolves when all concurrent background operations complete.

export function internal_getCurrentFunctionWaitUntil() {
  return state.waitUntilPromise
}
 
export function internal_runWithWaitUntil<T>(fn: () => T): T {
  const result = fn()
  if (
    result &&
    typeof result === 'object' &&
    'then' in result &&
    'finally' in result &&
    typeof result.then === 'function' &&
    typeof result.finally === 'function'
  ) {
    if (!state.waitUntilCounter) {
      // Create the promise for the next batch of waitUntil calls.
      state.waitUntilPromise = new Promise<void>((resolve) => {
        state.waitUntilResolve = resolve
      })
    }
    state.waitUntilCounter++
    return result.finally(() => {
      resolveOnePromise()
    })
  }
 
  return result

Sources: packages/next/src/server/web/internal-edge-wait-until.ts:32-58

Note

Regardless of the number of concurrent requests executing in the edge sandbox, resolveOnePromise ensures the primary waitUntilPromise remains active until waitUntilCounter drops exactly to zero.

Sources: packages/next/src/server/web/internal-edge-wait-until.ts:21-30

During adapter execution, requests identified as middleware (/middleware, /src/middleware, /proxy, /src/proxy) are wrapped with execution telemetry via MiddlewareSpan.execute. The adapter sets up a CloseController to schedule resource cleanup via setTimeout upon completion, initializes request and work stores using createRequestStoreForAPI and createWorkStore, and executes the handler within workAsyncStorage and workUnitAsyncStorage contexts.

response = await propagator(request, () => {
  // we only care to make async storage available for middleware
  const isMiddleware =
    params.page === '/middleware' ||
    params.page === '/src/middleware' ||
    params.page === '/proxy' ||
    params.page === '/src/proxy'
 
  if (isMiddleware) {
    // if we're in an edge function, we only get a subset of `nextConfig` (no `experimental`),
    // so we have to inject it via DefinePlugin.
    // in `next start` this will be passed normally (see `NextNodeServer.runMiddleware`).
 
    const waitUntil = event.waitUntil.bind(event)
    const closeController = new CloseController()
 
    return getTracer().trace(
      MiddlewareSpan.execute,
      {
        spanName: `middleware ${request.method}`,
        attributes: {
          'http.target': request.nextUrl.pathname,
          'http.method': request.method,
        },
      },
      async () => {
        try {
          const onUpdateCookies = (cookies: Array<string>) => {
            cookiesFromResponse = cookies
          }
          const previewProps = getEdgePreviewProps()
          const page = '/' // Fake Work
          const fallbackRouteParams = null
 
          const implicitTags = await getImplicitTags(
            page,
            request.nextUrl.pathname,
            fallbackRouteParams
          )
 
          const requestStore = createRequestStoreForAPI(
            request,
            request.nextUrl,
            implicitTags,
            onUpdateCookies,
            previewProps,
            // Edge route handlers can't use `"use cache"`, so there's no HMR
            // refresh hash to thread through here.
            undefined
          )
 
          const workStore = createWorkStore({
            page,
            renderOpts: {
              cacheLifeProfiles: proxyCacheLifeProfiles,
              // Proxy doesn't do static generation, so this value does not
              // apply here. 0 is a sentinel: if something ever reads it,
              // it'll surface loudly instead of silently using a misleading
              // default.
              staticPageGenerationTimeout: 0,
              cacheComponents: false,
              // Proxy doesn't run instant validation; the level value is
              // irrelevant here.
              // TODO: remove validationLevel and other global config from renderOpts
              validationLevel: 'warning',
              experimental: {
                isRoutePPREnabled: false,
                authInterrupts:
                  !!params.request.nextConfig?.experimental?.authInterrupts,
                // Proxy doesn't fill Cache Components entries, so this value
                // is never read. 0 is a sentinel: if something ever reads it,
                // the cache fill will time out immediately and surface the
                // bug.
                useCacheTimeout: 0,
                durableUseCacheEntries: false,
              },
              waitUntil,
              onClose: closeController.onClose.bind(closeController),
              onAfterTaskError: undefined,
            },

Sources: packages/next/src/server/web/adapter.ts:270-377

Route Module Edge Wrappers

Edge Route Module Wrapper Class

The EdgeRouteModuleWrapper class bridges edge request handlers and App Route modules. Constructed via its static wrap method, it accepts an AppRouteRouteModule instance along with configuration options like cacheHandlers and incrementalCacheHandler. The private constructor evaluates whether the route pathname is dynamic using isDynamicRoute.

export class EdgeRouteModuleWrapper {
  private readonly pageIsDynamic: boolean
 
  /**
   * The constructor is wrapped with private to ensure that it can only be
   * constructed by the static wrap method.
   *
   * @param routeModule the route module to wrap
   */
  private constructor(
    private readonly routeModule: AppRouteRouteModule,
    private readonly cacheHandlers: Record<string, CacheHandler>
  ) {
    this.pageIsDynamic = isDynamicRoute(routeModule.definition.pathname)
  }

Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:35-49

Parameter / PropertyTypeDescription
routeModuleAppRouteRouteModuleThe target App Route module being executed
cacheHandlersRecord<string, CacheHandler>Custom cache handlers registered for the edge runtime
incrementalCacheHandlertypeof IncrementalCacheHandlerOptional incremental cache class reference
pageIsDynamicbooleanIndicates if the route definition pathname contains dynamic segments
export interface WrapOptions {
  page: string
  cacheHandlers?: Record<string, CacheHandler>
  incrementalCacheHandler?: typeof IncrementalCacheHandler
}
 
/**
 * EdgeRouteModuleWrapper is a wrapper around a route module.
 *
 * Note that this class should only be used in the edge runtime.
 */
export class EdgeRouteModuleWrapper {
  private readonly pageIsDynamic: boolean
 
  /**
   * The constructor is wrapped with private to ensure that it can only be
   * constructed by the static wrap method.
   *
   * @param routeModule the route module to wrap
   */
  private constructor(
    private readonly routeModule: AppRouteRouteModule,
    private readonly cacheHandlers: Record<string, CacheHandler>
  ) {
    this.pageIsDynamic = isDynamicRoute(routeModule.definition.pathname)
  }

Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:24-49

Request Execution and Context Construction

When the wrapped handler executes, it initializes server utilities, loads custom cache handlers based on nextConfig.cacheMaxMemorySize, and maps dynamic route parameters using normalizeDynamicRouteParams. It constructs an AppRouteRouteHandlerContext containing route parameters, preview properties, and render options.

private async handler(
  request: NextRequestHint,
  evt: NextFetchEvent
): Promise<Response> {
  const utils = getServerUtils({
    pageIsDynamic: this.pageIsDynamic,
    page: this.routeModule.definition.pathname,
    basePath: request.nextUrl.basePath,
    // We don't need the `handleRewrite` util, so can just pass an empty object
    rewrites: {},
    // only used for rewrites, so setting an arbitrary default value here
    caseSensitive: false,
  })
 
  const { nextConfig } = this.routeModule.getNextConfigEdge(
    new WebNextRequest(request)
  )
  initializeCacheHandlers(nextConfig.cacheMaxMemorySize)
  for (const [kind, cacheHandler] of Object.entries(this.cacheHandlers)) {
    setCacheHandler(kind, cacheHandler)
  }
 
  const { params } = utils.normalizeDynamicRouteParams(
    searchParamsToUrlQuery(request.nextUrl.searchParams),
    false
  )
 
  const waitUntil = evt.waitUntil.bind(evt)
  const closeController = new CloseController()
 
  const previewProps = getEdgePreviewProps()
 
  // Create the context for the handler. This contains the params from the
  // match (if any).
  const context: AppRouteRouteHandlerContext = {
    params,
    previewProps,
    renderOpts: {
      waitUntil,
      onClose: closeController.onClose.bind(closeController),
      onAfterTaskError: undefined,
      cacheComponents: !!process.env.__NEXT_CACHE_COMPONENTS,
      // Edge runtime doesn't run instant validation; the level value is
      // irrelevant here.
      // TODO: Remove validationLevel and other global config from renderOpts
      validationLevel: 'warning',
      experimental: {
        authInterrupts: !!process.env.__NEXT_EXPERIMENTAL_AUTH_INTERRUPTS,
        // Edge runtime doesn't support Cache Components, so this value is
        // never read. 0 is a sentinel: if something ever reads it, the cache
        // fill will time out immediately and surface the bug.
        useCacheTimeout: 0,
        durableUseCacheEntries: false,
      },
      cacheLifeProfiles: nextConfig.cacheLife,
      // Edge runtime doesn't do static generation, so this value does not
      // apply here. 0 is a sentinel: if something ever reads it, it'll
      // surface loudly instead of silently using a misleading default.
      staticPageGenerationTimeout: 0,
    },
    sharedContext: {
      buildId: '', // TODO: Populate this properly.
      deploymentId: '', // TODO: Populate this properly.
    },
  }

Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:83-147

Caution

In the edge runtime, staticPageGenerationTimeout and useCacheTimeout are set to sentinel values of 0. Because edge environments do not support static generation or cache components, any accidental read forces an immediate timeout to expose the invalid configuration loudly.

Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:129-142

Background Task Handling and Response Tracking

After invoking this.routeModule.handle(request, context), the wrapper gathers pending background work by combining internal_getCurrentFunctionWaitUntil() and context.renderOpts.pendingWaitUntil into evt.waitUntil(). If the response lacks a body, a CloseController dispatches closure via setTimeout with a delay of zero; otherwise, trackStreamConsumed wraps the response body to trigger cleanup once consumption finishes.

  // Get the response from the handler.
  let res = await this.routeModule.handle(request, context)
 
  const waitUntilPromises = [internal_getCurrentFunctionWaitUntil()]
  if (context.renderOpts.pendingWaitUntil) {
    waitUntilPromises.push(context.renderOpts.pendingWaitUntil)
  }
  evt.waitUntil(Promise.all(waitUntilPromises))
 
  if (!res.body) {
    // we can delay running it until a bit later --
    // if it's needed, we'll have a `waitUntil` lock anyway.
    setTimeout(() => closeController.dispatchClose(), 0)
  } else {
    // NOTE: if this is a streaming response, onClose may be called later,
    // so we can't rely on `closeController.listeners` -- it might be 0 at this point.
    const trackedBody = trackStreamConsumed(res.body, () =>
      closeController.dispatchClose()
    )
    res = new Response(trackedBody, {
      status: res.status,
      statusText: res.statusText,
      headers: res.headers,
    })
  }
 
  return res
}

Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:149-176

Note

For streaming responses, onClose cannot rely on pre-existing static listener counts because cleanup may be deferred until the stream body is fully read or consumed.

Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:163-167

Edge SSR and Middleware Templates

Pages SSR and App SSR Entrypoint Templates

Next.js generates dedicated edge entrypoint templates for Server-Side Rendering under both the Pages and App routers. The Pages SSR template (edge-ssr.ts) imports global polyfills, the adapter utility, and userland modules including Document, VAR_MODULE_APP, and VAR_USERLAND. It instantiates a Pages route module with pageRouteModuleOptions, configures cache handlers using nextConfig.cacheMaxMemorySize, and maps responses through renderResultToResponse.

import '../../server/web/globals'
import {
  adapter,
  type EdgeHandler,
  type NextRequestHint,
} from '../../server/web/adapter'
import { IncrementalCache } from '../../server/lib/incremental-cache'
import * as cacheHandlers from '../../server/use-cache/handlers'
 
import Document from 'VAR_MODULE_DOCUMENT'
import * as appMod from 'VAR_MODULE_APP'
import * as userlandPage from 'VAR_USERLAND'
import * as userlandErrorPage from 'VAR_MODULE_GLOBAL_ERROR'
 
declare const userland500Page: any
declare const incrementalCacheHandler: any
// OPTIONAL_IMPORT:* as userland500Page
// OPTIONAL_IMPORT:incrementalCacheHandler
 
import RouteModule, {
  type PagesRouteHandlerContext,
} from '../../server/route-modules/pages/module'
import { WebNextRequest, WebNextResponse } from '../../server/base-http/web'
 
import type { NextFetchEvent } from '../../server/web/spec-extension/fetch-event'
import type RenderResult from '../../server/render-result'
import type { RenderResultMetadata } from '../../server/render-result'
import {
  getTracer,
  SpanKind,
  SpanStatusCode,
  type Span,
} from '../../server/lib/trace/tracer'
import { BaseServerSpan } from '../../server/lib/trace/constants'
import { HTML_CONTENT_TYPE_HEADER } from '../../lib/constants'
import type { RequestMeta } from '../../server/request-meta'
import { toNodeOutgoingHttpHeaders } from '../../server/web/utils'
 
// injected by the loader afterwards.
declare const pageRouteModuleOptions: any
declare const errorRouteModuleOptions: any
declare const user500RouteModuleOptions: any
// INJECT:pageRouteModuleOptions
// INJECT:errorRouteModuleOptions
// INJECT:user500RouteModuleOptions
// INJECT_RAW:cacheHandlerImports
 
const pageMod = {
  ...userlandPage,
  routeModule: new RouteModule({
    ...pageRouteModuleOptions,
    components: {
      App: appMod.default,
      Document,
    },
    userland: userlandPage,
    distDir: process.env.__NEXT_RELATIVE_DIST_DIR || '',
    relativeProjectDir: process.env.__NEXT_RELATIVE_PROJECT_DIR || '',
  }),
}

Sources: packages/next/src/build/templates/edge-ssr.ts:1-60

cacheHandlers.initializeCacheHandlers(nextConfig.cacheMaxMemorySize)
// INJECT_RAW:cacheHandlerRegistration
 
const renderContext: PagesRouteHandlerContext = {
  page: srcPage,
  query,
  params,
 
  sharedContext: {
    buildId,
    deploymentId,
    clientAssetToken,
    customServer: undefined,
  },
 
  renderContext: {
    isFallback: false,
    isDraftMode: false,
    developmentNotFoundSourcePage: undefined,
  },
 
  renderOpts: {
    params,
    page: srcPage,
    supportsDynamicResponse: true,
    Component: pageMod.Component,
    ComponentMod: pageMod,
    pageConfig: pageMod.pageConfig,
    routeModule: pageMod.routeModule,
    previewProps,
    basePath: nextConfig.basePath,
    assetPrefix: nextConfig.assetPrefix,
    images: nextConfig.images,
    optimizeCss: nextConfig.experimental.optimizeCss,
    nextConfigOutput: nextConfig.output,
    nextScriptWorkers: nextConfig.experimental.nextScriptWorkers,
    disableOptimizedLoading: nextConfig.experimental.disableOptimizedLoading,
    domainLocales: nextConfig.i18n?.domains,
    distDir: '',
    crossOrigin: nextConfig.crossOrigin ? nextConfig.crossOrigin : undefined,
    largePageDataBytes: nextConfig.experimental.largePageDataBytes,
 
    isExperimentalCompile: nextConfig.experimental.isExperimentalCompile,
    // `htmlLimitedBots` is passed to server as serialized config in string format
    experimental: {
      clientTraceMetadata: nextConfig.experimental.clientTraceMetadata,
    },
 
    buildManifest,
    subresourceIntegrityManifest,
    reactLoadableManifest,
    dynamicCssManifest,
  },
}

Sources: packages/next/src/build/templates/edge-ssr.ts:129-182

Similarly, the App SSR template (edge-ssr-app.ts) populates the React Server Component (RSC) manifest singletons by reading self.__RSC_MANIFEST and self.__RSC_SERVER_MANIFEST. It wraps page execution with AppPageRouteModule, detects server actions via getIsPossibleServerAction, and sets up incremental caching and stream tracking controllers.

import '../../server/web/globals'
import {
  adapter,
  type EdgeHandler,
  type NextRequestHint,
} from '../../server/web/adapter'
import { IncrementalCache } from '../../server/lib/incremental-cache'
 
import * as pageMod from 'VAR_USERLAND'
 
import { setManifestsSingleton } from '../../server/app-render/manifests-singleton'
import * as cacheHandlers from '../../server/use-cache/handlers'
import { BaseServerSpan } from '../../server/lib/trace/constants'
import {
  getTracer,
  SpanKind,
  SpanStatusCode,
  type Span,
} from '../../server/lib/trace/tracer'
import { WebNextRequest, WebNextResponse } from '../../server/base-http/web'
import type { NextFetchEvent } from '../../server/web/spec-extension/fetch-event'
import type {
  AppPageRouteHandlerContext,
  AppPageRouteModule,
  DevRenderContext,
  RouteMatch,
} from '../../server/route-modules/app-page/module.compiled'
import type { AppPageRenderResultMetadata } from '../../server/render-result'
import type RenderResult from '../../server/render-result'
import { getIsPossibleServerAction } from '../../server/lib/server-action-request-meta'
import { getBotType } from '../../shared/lib/router/utils/is-bot'
import { shouldServeStreamingMetadata } from '../../server/lib/streaming-metadata'
import { interopDefault } from '../../lib/interop-default'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
import { checkIsOnDemandRevalidate } from '../../server/api-utils'
import { CloseController } from '../../server/web/web-on-close'
import { parseMaxPostponedStateSize } from '../../shared/lib/size-limit'
import { toNodeOutgoingHttpHeaders } from '../../server/web/utils'
import type { RequestMeta } from '../../server/request-meta'
import { createDevRenderContext } from '../../server/route-modules/app-page/dev-render-context'
import {
  parseRequestHeaders,
  type ParsedRequestHeaders,
} from '../../server/route-modules/app-page/parse-request-headers'
 
declare const incrementalCacheHandler: any
// OPTIONAL_IMPORT:incrementalCacheHandler
// INJECT_RAW:cacheHandlerImports
 
const maybeJSONParse = (str?: string) => (str ? JSON.parse(str) : undefined)
 
const rscManifest = self.__RSC_MANIFEST?.['VAR_PAGE']
const rscServerManifest = maybeJSONParse(self.__RSC_SERVER_MANIFEST)
 
if (rscManifest && rscServerManifest) {
  setManifestsSingleton({
    page: 'VAR_PAGE',
    clientReferenceManifest: rscManifest,
    serverActionsManifest: rscServerManifest,
  })
}
 
export const ComponentMod = pageMod
 

Sources: packages/next/src/build/templates/edge-ssr-app.ts:1-64

// Initialize the cache handlers interface.
cacheHandlers.initializeCacheHandlers(nextConfig.cacheMaxMemorySize)
// INJECT_RAW:cacheHandlerRegistration
 
const isPossibleServerAction = getIsPossibleServerAction(req)
const userAgent = req.headers.get('User-Agent') || ''
const botType = getBotType(userAgent)
const { isOnDemandRevalidate } = checkIsOnDemandRevalidate(
  req.headers,
  previewProps
)
 
const closeController = new CloseController()
 
const routeMatch: RouteMatch = { resolvedPathname }
const dev: DevRenderContext | undefined = createDevRenderContext(baseReq)
const renderContextBase: Omit<
  AppPageRouteHandlerContext,
  'parsedRequestHeaders'
> = {
  page: normalizedSrcPage,
  routeMatch,
  query,
  params,
  dev,
 
  sharedContext: {
    buildId,
    deploymentId,
    clientAssetToken,
  },

Sources: packages/next/src/build/templates/edge-ssr-app.ts:106-136

Warning

In the edge SSR application template, serverComponentsHmrCancellation is explicitly set to false. Because the edge runtime lacks a Node response-close signal, HMR cancellation is treated as a no-op during development.

Sources: packages/next/src/build/templates/edge-ssr-app.ts:201-204

Middleware and Edge API Templates

The middleware template (middleware.ts) and Pages Edge API template (pages-edge-api.ts) handle lightweight edge routing and API handlers. The middleware template resolves userland code as either mod.proxy, mod.middleware, or mod.default. It verifies that the export is a function, throwing a ProxyMissingExportError if validation fails.

const mod = { ..._mod }
 
const page: string = 'VAR_DEFINITION_PAGE'
const isProxy = page === '/proxy' || page === '/src/proxy'
const handlerUserland = (isProxy ? mod.proxy : mod.middleware) || mod.default
 
class ProxyMissingExportError extends Error {
  constructor(message: string) {
    super(message)
    // Stack isn't useful here, remove it considering it spams logs during development.
    this.stack = ''
  }
}
 
// TODO: This spams logs during development. Find a better way to handle this.
// Removing this will spam "fn is not a function" logs which is worse.
if (typeof handlerUserland !== 'function') {
  throw new ProxyMissingExportError(
    `The ${isProxy ? 'Proxy' : 'Middleware'} file "${page}" must export a function named \`${isProxy ? 'proxy' : 'middleware'}\` or a default function.`
  )
}

Sources: packages/next/src/build/templates/middleware.ts:17-37

Export TargetCondition / CheckError or Fallback Behavior
mod.proxy`page === '/proxy'
mod.middlewareStandard middleware pathRequired to be a valid function export
mod.defaultFallback export optionThrows ProxyMissingExportError if type is not a function
const mod = { ..._mod }
 
const page: string = 'VAR_DEFINITION_PAGE'
const isProxy = page === '/proxy' || page === '/src/proxy'
const handlerUserland = (isProxy ? mod.proxy : mod.middleware) || mod.default
 
class ProxyMissingExportError extends Error {
  constructor(message: string) {
    super(message)
    // Stack isn't useful here, remove it considering it spams logs during development.
    this.stack = ''
  }
}
 
// TODO: This spams logs during development. Find a better way to handle this.
// Removing this will spam "fn is not a function" logs which is worse.
if (typeof handlerUserland !== 'function') {
  throw new ProxyMissingExportError(
    `The ${isProxy ? 'Proxy' : 'Middleware'} file "${page}" must export a function named \`${isProxy ? 'proxy' : 'middleware'}\` or a default function.`
  )
}

Sources: packages/next/src/build/templates/middleware.ts:17-37

The errorHandledHandler wrapper intercepts runtime failures in middleware or proxy code. During non-production builds, if an error is identified as a router error via isNextRouterError, its message is modified to state that the Next.js navigation API is forbidden within middleware or proxy scopes before reporting the error through edgeInstrumentationOnRequestError.

function errorHandledHandler(fn: AdapterOptions['handler']) {
  return async (...args: Parameters<AdapterOptions['handler']>) => {
    try {
      return await fn(...args)
    } catch (err) {
      // In development, error the navigation API usage in runtime,
      // since it's not allowed to be used in proxy as it's outside of react component tree.
      if (process.env.NODE_ENV !== 'production') {
        if (isNextRouterError(err)) {
          err.message = `Next.js navigation API is not allowed to be used in ${isProxy ? 'Proxy' : 'Middleware'}.`
          throw err
        }
      }
      const req = args[0]
      const url = new URL(req.url)
      const resource = url.pathname + url.search
      await edgeInstrumentationOnRequestError(
        err,
        {
          path: resource,
          method: req.method,
          headers: Object.fromEntries(req.headers.entries()),
        },
        {
          routerKind: 'Pages Router',
          routePath: '/proxy',
          routeType: 'proxy',
          revalidateReason: undefined,
        }
      )
 
      throw err
    }
  }
}

Sources: packages/next/src/build/templates/middleware.ts:41-75

Note

ProxyMissingExportError clears its error stack property during construction to eliminate log spamming caused by routine export validation checks during local development.

Sources: packages/next/src/build/templates/middleware.ts:23-29

Was this page helpful?