---
title: "Edge Runtime"
description: "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 ..."
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/edge-runtime"
---

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

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

- [packages/next/src/server/web/sandbox/sandbox.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/sandbox.ts)
- [packages/next/src/build/templates/edge-ssr.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/edge-ssr.ts)
- [packages/next/src/server/next-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next-server.ts)
- [packages/next/src/build/templates/edge-ssr-app.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/edge-ssr-app.ts)
- [packages/next/src/build/templates/edge-app-route.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/edge-app-route.ts)
- [packages/next/src/server/web/sandbox/context.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/context.ts)
- [packages/next/src/server/web/adapter.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/adapter.ts)
- [packages/next/src/build/templates/middleware.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/middleware.ts)
- [packages/next/src/build/templates/app-route.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/app-route.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/server/dev/next-dev-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/next-dev-server.ts)
- [packages/next/src/build/webpack/plugins/middleware-plugin.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/plugins/middleware-plugin.ts)
- [packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts)
- [packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts)
- [packages/next/src/build/templates/pages-edge-api.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/pages-edge-api.ts)
- [packages/next/src/server/web/edge-route-module-wrapper.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/edge-route-module-wrapper.ts)
- [packages/next/src/build/templates/pages-api.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/pages-api.ts)
- [packages/next/src/server/route-modules/route-module.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/route-modules/route-module.ts)
- [packages/next/src/build/templates/edge-wrapper.js](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/edge-wrapper.js)
- [packages/next/src/build/webpack/loaders/next-edge-function-loader.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-edge-function-loader.ts)
- [packages/next/src/build/webpack/loaders/next-middleware-wasm-loader.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-middleware-wasm-loader.ts)
- [packages/next/src/server/web/internal-edge-wait-until.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/internal-edge-wait-until.ts)
- [packages/next/src/build/webpack/loaders/next-middleware-asset-loader.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-middleware-asset-loader.ts)
- [packages/next/src/server/web/globals.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/globals.ts)
- [packages/next/src/server/route-modules/pages-api/module.compiled.js](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/route-modules/pages-api/module.compiled.js)
- [packages/next/src/server/web/spec-extension/request.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/request.ts)
- [packages/next/src/server/web/spec-extension/response.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/response.ts)
</details>

## 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](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/build-system/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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/sandbox.ts#L1-L44), [packages/next/src/server/web/sandbox/context.ts:259-282](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/context.ts#L259-L282), [packages/next/src/build/webpack/plugins/middleware-plugin.ts:44-74](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/plugins/middleware-plugin.ts#L44-L74), [packages/next/src/server/web/adapter.ts:259-278](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/adapter.ts#L259-L278), [packages/next/src/server/web/edge-route-module-wrapper.ts:35-81](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/edge-route-module-wrapper.ts#L35-L81)

## 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](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/client-runtime/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 Name | Source File | Key Responsibilities |
| :--- | :--- | :--- |
| `EdgeAppRouteLoader` | `packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts` | Configures `nextEdgeSSR` build info, parses base64 middleware configurations, and sets up cache handlers for App Router route modules. |
| `edgeSSRLoader` | `packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts` | Resolves absolute paths for documents, error boundaries, and 500 pages while mapping dist folders to ESM variants. |
| `nextEdgeFunctionLoader` | `packages/next/src/build/webpack/loaders/next-edge-function-loader.ts` | Injects global web polyfills, wraps page handlers using `wrapApiHandler`, and exports an adapter instance for Edge API routes. |
| `MiddlewareWasmLoader` | `packages/next/src/build/webpack/loaders/next-middleware-wasm-loader.ts` | Computes a SHA-1 hash of binary WebAssembly files, emits chunks into `server/edge-chunks/`, and exposes module bindings. |
| `MiddlewareAssetLoader` | `packages/next/src/build/webpack/loaders/next-middleware-asset-loader.ts` | Interpolates 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts#L57-L117), [packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts:104-228](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts#L104-L228), [packages/next/src/build/webpack/loaders/next-edge-function-loader.ts:15-71](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-edge-function-loader.ts#L15-L71), [packages/next/src/build/webpack/loaders/next-middleware-wasm-loader.ts:8-15](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-edge-function-loader.ts#L8-L15), [packages/next/src/build/webpack/loaders/next-middleware-asset-loader.ts:4-17](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-middleware-asset-loader.ts#L4-L17)

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-middleware-wasm-loader.ts#L8-L15)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/plugins/middleware-plugin.ts#L44-L64)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/plugins/middleware-plugin.ts#L105-L158)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/globals.ts#L57-L108)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/globals.ts#L57-L82)

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

```typescript
  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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/context.ts#L264-L464)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/context.ts#L289-L305)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/context.ts#L259-L399)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/context.ts#L324-L349)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/sandbox.ts#L72-L109)

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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/sandbox.ts#L111-L163)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/sandbox.ts#L23-L27), [packages/next/src/server/web/sandbox/sandbox.ts:152-154](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/sandbox.ts#L152-L154)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/sandbox.ts#L49-L70)

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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/sandbox/context.ts#L467-L474)

## 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'`.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/request.ts#L14-L52)

`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 Method | Default Status / Header | Description |
| --- | --- | --- |
| `NextResponse.json` | Standard JSON init | Instantiates a response wrapping a JSON body |
| `NextResponse.redirect` | Status `307` (validates `301`, `302`, `303`, `307`, `308`) | Sets the `Location` header to the validated target URL |
| `NextResponse.rewrite` | Header `x-middleware-rewrite` | Sets the rewrite destination and handles middleware request overrides |
| `NextResponse.next` | Header `x-middleware-next: '1'` | Instructs the router to continue to the next middleware or route |

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/response.ts#L36-L153)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/response.ts#L10-L10), [packages/next/src/server/web/spec-extension/response.ts:118-123](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/response.ts#L118-L123)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/internal-edge-wait-until.ts#L32-L58)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/internal-edge-wait-until.ts#L21-L30)

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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/adapter.ts#L270-L377)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/edge-route-module-wrapper.ts#L35-L49)

| Parameter / Property | Type | Description |
| --- | --- | --- |
| `routeModule` | `AppRouteRouteModule` | The target App Route module being executed |
| `cacheHandlers` | `Record<string, CacheHandler>` | Custom cache handlers registered for the edge runtime |
| `incrementalCacheHandler` | `typeof IncrementalCacheHandler` | Optional [incremental cache](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/incremental-cache) class reference |
| `pageIsDynamic` | `boolean` | Indicates if the route definition pathname contains dynamic segments |

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/edge-route-module-wrapper.ts#L24-L49)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/edge-route-module-wrapper.ts#L83-L147)

> [!CAUTION]
> In the edge runtime, `staticPageGenerationTimeout` and `useCacheTimeout` are set to sentinel values of `0`. Because edge environments do not support [static generation](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/build-system/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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/edge-route-module-wrapper.ts#L129-L142)

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

```typescript
  // 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/edge-route-module-wrapper.ts#L149-L176)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/edge-route-module-wrapper.ts#L163-L167)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/edge-ssr.ts#L1-L60)

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/edge-ssr.ts#L129-L182)

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](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/server-actions) via `getIsPossibleServerAction`, and sets up incremental caching and stream tracking controllers.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/edge-ssr-app.ts#L1-L64)

```typescript
// 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/edge-ssr-app.ts#L106-L136)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/edge-ssr-app.ts#L201-L204)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/middleware.ts#L17-L37)

| Export Target | Condition / Check | Error or Fallback Behavior |
| --- | --- | --- |
| `mod.proxy` | `page === '/proxy' || page === '/src/proxy'` | Falls back to `mod.middleware` or `mod.default` |
| `mod.middleware` | Standard middleware path | Required to be a valid function export |
| `mod.default` | Fallback export option | Throws `ProxyMissingExportError` if type is not a function |

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/middleware.ts#L17-L37)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/middleware.ts#L41-L75)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/middleware.ts#L23-L29)

## Related

- [Server Architecture](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/server-architecture)
- [Route Handlers](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/route-handlers)


## Sitemap

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