Type something to search...
How to Run Next.js on the Edge Runtime?

How to Run Next.js on the Edge Runtime?

For a few years, "run it on the edge" was the default performance advice for Next.js. Add one line, export const runtime = "edge", and your route handler or page would run in a lightweight V8 isolate close to the visitor instead of a Node.js server in one region, with almost no cold start. Plenty of tutorials still recommend it, and plenty of production codebases still have that line scattered across their app directory.

The picture has changed. In Next.js 16, the Edge Runtime is deprecated as a route segment option, Proxy (the renamed Middleware) runs on Node.js by default, and Cache Components does not support the Edge Runtime at all. If you are starting a new project, or upgrading an old one, you need to know what the Edge Runtime still does, where it still works, and when you should move away from it.

This article covers the Edge Runtime from start to finish: what it is and how it differs from the Node.js runtime, how to opt routes and middleware into it, which APIs are available and which are not, its current status in Next.js 16, practical edge-friendly code patterns, and a step-by-step migration back to the Node.js runtime.

What the Edge Runtime Is

Next.js has two server runtimes:

  • The Node.js runtime (the default). Your server code runs in Node.js with access to every Node API: the filesystem, crypto, net, native modules, and the entire npm ecosystem.
  • The Edge Runtime. A restricted JavaScript environment built on V8 isolates and Web Standard APIs (fetch, Request, Response, Headers, URL, Web Crypto, streams). It is the same kind of environment used by Vercel Edge Functions and Cloudflare Workers.

The trade-off is simple: the Edge Runtime starts faster and can be deployed to many regions cheaply, but it can do much less.

FeatureNode.js runtimeEdge Runtime
Cold startSlowerVery fast
Node.js APIs (fs, net, child_process)YesNo
Native npm modulesYesNo
Web APIs (fetch, streams, Web Crypto)YesYes
Database drivers over TCPYesNo, HTTP-based drivers only
Incremental Static RegenerationYesNo
Cache Components ("use cache")YesNo
Route segment revalidateYesNo
Code size limitsGenerousStrict, depends on the platform
Status in Next.js 16Default, recommendedDeprecated for route segments

Where the Edge Runtime code actually runs depends on your host. On Vercel it ran as Edge Functions. When you self-host with next start, edge routes run inside your Node.js server in a sandbox that emulates the Edge Runtime, so you get the restrictions without the geographic distribution. That surprises a lot of people who self-host.

The Edge Runtime Status in Next.js 16

Here is the current state, version by version, because this is where most of the confusion comes from:

VersionWhat changed
Next.js 13runtime = "edge" available for App Router pages, layouts, and route handlers
Next.js 15runtime = "experimental-edge" errors, use "edge". request.geo and request.ip removed from NextRequest
Next.js 15.5Middleware can use the Node.js runtime (stable)
Next.js 16Middleware renamed to Proxy. Proxy runs on Node.js only. runtime = "edge" deprecated for route segments. Cache Components requires Node.js

In practical terms, on Next.js 16:

  1. export const runtime = "edge" still works in pages, layouts, and route handlers, but it is deprecated and Next.js warns about it. The documented guidance is to remove the export.
  2. proxy.ts cannot use the Edge Runtime. Its runtime is always Node.js and cannot be configured. Setting runtime in a Proxy file throws an error.
  3. middleware.ts still runs on the Edge Runtime. The middleware filename is deprecated, but the upgrade guide explicitly says to keep using it if you need edge execution for request interception.
  4. Cache Components and the Edge Runtime are incompatible. If you enable cacheComponents: true, every route must use Node.js.

So "how to run Next.js on the Edge Runtime" now has two honest answers: here is how to do it, and here is why you probably want to plan your way off it.

How to Opt a Route Into the Edge Runtime

Route Handlers

The most common use was API endpoints that do light work, such as proxying a request or returning JSON from an HTTP API:

// app/api/time/route.ts
export const runtime = "edge"; // deprecated in Next.js 16

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const timeZone = searchParams.get("tz") ?? "UTC";

  const now = new Intl.DateTimeFormat("en-US", {
    timeZone,
    dateStyle: "full",
    timeStyle: "long",
  }).format(new Date());

  return Response.json({ timeZone, now });
}

Everything this handler uses (Request, URL, Intl, Response.json) is a Web Standard API, so it runs unchanged on either runtime. That is the key insight for the migration later in this article: well-written edge code is also valid Node.js code.

Pages and Layouts

The same export works in a page or layout file and applies to that segment and its children:

// app/status/page.tsx
export const runtime = "edge"; // deprecated in Next.js 16

async function getStatus() {
  const res = await fetch("https://www.githubstatus.com/api/v2/status.json", {
    cache: "no-store",
  });
  if (!res.ok) throw new Error("Status check failed");
  return (await res.json()) as { status: { description: string } };
}

export default async function StatusPage() {
  const data = await getStatus();
  return (
    <main className="container py-16">
      <h1 className="h2">GitHub status</h1>
      <p className="mt-4">{data.status.description}</p>
    </main>
  );
}

Note what is missing: no export const revalidate. Segment-level revalidation is not available on the Edge Runtime, and neither is ISR. Every request renders on demand.

Middleware on the Edge Runtime

If you need request interception on the Edge Runtime in Next.js 16, use the deprecated middleware.ts filename at the project root (or inside src/):

// middleware.ts
import { NextResponse, type NextRequest } from "next/server";

const SUPPORTED_LOCALES = ["en", "bn", "fr"] as const;

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  const hasLocale = SUPPORTED_LOCALES.some(
    (locale) => pathname === `/${locale}` || pathname.startsWith(`/${locale}/`),
  );
  if (hasLocale) return NextResponse.next();

  const preferred = request.headers
    .get("accept-language")
    ?.split(",")[0]
    ?.slice(0, 2);
  const locale = SUPPORTED_LOCALES.find((l) => l === preferred) ?? "en";

  const url = request.nextUrl.clone();
  url.pathname = `/${locale}${pathname}`;
  return NextResponse.redirect(url);
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)"],
};

The same file renamed to proxy.ts with the function renamed to proxy runs on Node.js instead. Since this code uses only Web APIs, it works identically in both. If the multilingual routing pattern is what you are after, how to create a multi-language website with Next.js goes deeper.

What You Can and Cannot Use on the Edge

Available APIs

The Edge Runtime supports Web Standard APIs:

  • Network: fetch, Request, Response, Headers, FormData, Blob, File, URL, URLSearchParams, WebSocket
  • Encoding: TextEncoder, TextDecoder, atob, btoa
  • Streams: ReadableStream, WritableStream, TransformStream
  • Crypto: crypto.subtle, crypto.randomUUID(), crypto.getRandomValues()
  • Other: setTimeout, structuredClone, AbortController, Intl, and the standard JavaScript built-ins
  • Next.js polyfills: AsyncLocalStorage
  • Environment variables: process.env works in both next dev and next build

Unsupported APIs

  • Native Node.js modules. No fs, path file reads, child_process, net, or tls.
  • require. Use ES Modules imports.
  • Dynamic code evaluation. eval, new Function(string), WebAssembly.compile, and WebAssembly.instantiate with a buffer are disabled.
  • npm packages that depend on Node APIs. Many ORMs, older JWT libraries, and SDKs that use crypto from Node will fail at build time or at runtime.

Writing Edge-Compatible Code

The trick is to reach for the Web Platform first. For example, signing a token with Web Crypto instead of Node's crypto module:

// lib/sign.ts
const encoder = new TextEncoder();

async function getKey(secret: string) {
  return crypto.subtle.importKey(
    "raw",
    encoder.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign", "verify"],
  );
}

function toHex(buffer: ArrayBuffer): string {
  return Array.from(new Uint8Array(buffer))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

export async function sign(value: string, secret: string): Promise<string> {
  const key = await getKey(secret);
  const signature = await crypto.subtle.sign(
    "HMAC",
    key,
    encoder.encode(value),
  );
  return `${value}.${toHex(signature)}`;
}

export async function verify(
  signed: string,
  secret: string,
): Promise<string | null> {
  const index = signed.lastIndexOf(".");
  if (index === -1) return null;

  const value = signed.slice(0, index);
  const expected = await sign(value, secret);
  return expected === signed ? value : null;
}

This module runs on the Edge Runtime, in Node.js 20 and later (which has a global crypto), and in the browser. For authentication tokens, a maintained library such as jose is a better choice than rolling your own, and it is built on the same Web Crypto API. See how to handle authentication tokens in Next.js for the broader pattern.

Detecting the Current Runtime

When shared code must behave differently by runtime, check process.env.NEXT_RUNTIME, which is "nodejs" or "edge". The classic place for this is instrumentation.ts:

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    await import("./instrumentation.node");
  }

  if (process.env.NEXT_RUNTIME === "edge") {
    await import("./instrumentation.edge");
  }
}

Using a dynamic import() inside the check keeps Node-only code out of the edge bundle.

Databases on the Edge

Traditional database drivers open TCP connections, which the Edge Runtime cannot do. If you must query a database from an edge route, use a driver that talks HTTP or WebSockets, such as the serverless drivers offered by Neon, PlanetScale, Turso, or Upstash, or put your data behind an HTTP API. If your data lives in a single region anyway, running the route at the edge often makes it slower: every query travels from the edge location back to the database region.

Why Next.js Moved Away From the Edge Runtime

The Edge Runtime was a good idea with real costs, and those costs pushed Next.js back to Node.js:

  • Data locality beats compute locality. Most apps read from a database in one region. Rendering in Sydney and then querying a database in Virginia adds round trips instead of removing them.
  • Two runtimes, two sets of bugs. Libraries had to support both, and developers kept hitting "module not found: can't resolve 'fs'" errors from a transitive dependency.
  • Missing features. ISR, segment revalidate, and Cache Components never worked on the edge.
  • Node.js got faster. Improvements in serverless platforms and Fluid-style compute narrowed the cold-start gap that originally justified the Edge Runtime.

The recommended architecture in Next.js 16 is: render on Node.js close to your data, cache aggressively, serve static and cached output from a CDN, and use Proxy for lightweight request handling.

How to Migrate Edge Routes to the Node.js Runtime

Step 1: Find Every Edge Export

grep -rn "runtime = ['\"]edge['\"]\|runtime: ['\"]edge['\"]" app src --include=*.ts --include=*.tsx

Step 2: Remove the Export

Delete export const runtime = "edge" from each file. The default runtime is Node.js, so you do not need to add export const runtime = "nodejs".

// app/api/time/route.ts
// Before
export const runtime = "edge";

// After: no runtime export, Node.js is the default
export async function GET(request: Request) {
  // unchanged
}

Edge code that only used Web APIs needs no other change.

Step 3: Restore the Caching You Gave Up

Routes that ran on the edge could not use ISR. On Node.js they can. A status page that rendered on every request can now be cached and refreshed periodically. In the previous caching model:

// app/status/page.tsx
export const revalidate = 60;

export default async function StatusPage() {
  const res = await fetch("https://www.githubstatus.com/api/v2/status.json");
  const data = (await res.json()) as { status: { description: string } };
  return <p>{data.status.description}</p>;
}

Or with Cache Components enabled:

// app/status/page.tsx
import { cacheLife } from "next/cache";

async function getStatus() {
  "use cache";
  cacheLife("minutes");
  const res = await fetch("https://www.githubstatus.com/api/v2/status.json");
  return (await res.json()) as { status: { description: string } };
}

export default async function StatusPage() {
  const data = await getStatus();
  return <p>{data.status.description}</p>;
}

Step 4: Move Middleware to Proxy

Run the official codemod, which renames the file and the exported function:

npx @next/codemod@latest middleware-to-proxy .

If you renamed manually, the result looks like this:

// proxy.ts
import { NextResponse, type NextRequest } from "next/server";

export function proxy(request: NextRequest) {
  if (
    !request.cookies.has("session") &&
    request.nextUrl.pathname.startsWith("/dashboard")
  ) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*"],
};

Remove any runtime key from the Proxy config, because it throws an error in Proxy. A bonus of the move: Proxy runs on Node.js, so you can now use Node-only libraries there if you need them.

Step 5: Replace request.geo and request.ip

If your edge code read request.geo or request.ip, it already broke in Next.js 15, when those properties were removed from NextRequest. Read the values from headers your platform sets, such as x-forwarded-for for the client IP, or use your host's helper package (on Vercel, @vercel/functions provides geolocation() and ipAddress()).

// proxy.ts
import { NextResponse, type NextRequest } from "next/server";

export function proxy(request: NextRequest) {
  const ip =
    request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
  const response = NextResponse.next();
  response.headers.set("x-client-ip", ip);
  return response;
}

Step 6: Test a Production Build

npm run build && npm run start

Check the build output for deprecation warnings, and confirm each former edge route returns the same response it did before.

When the Edge Runtime Still Makes Sense

There are still a few reasonable cases, especially on Next.js 15 and earlier or with platforms built around isolates:

  • Lightweight request interception in middleware.ts, such as redirects, A/B test bucketing, or bot filtering, when you deploy to a platform that runs middleware at the edge.
  • Responses with no data dependency, like returning a computed header or a redirect based on a cookie.
  • Existing code you cannot migrate yet. It keeps working; plan the migration rather than leaving the deprecation warning forever.

For everything else, the Node.js runtime is the better default. If you deploy to Vercel, see how to deploy a Next.js application to Vercel for how functions and regions are configured there.

Common Problems and Fixes

  • "Module not found: Can't resolve 'fs'" in an edge route. A dependency uses Node APIs. Remove the edge runtime export, or replace the dependency with a Web API alternative.
  • "Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime." A dependency evaluates code at runtime. If the statement is unreachable, allow the file with unstable_allowDynamic in the middleware config; otherwise move the route to Node.js.
  • Error after renaming middleware to proxy. You kept runtime: "edge" in the config. Proxy is always Node.js, so remove it, or keep the middleware.ts name if you truly need edge execution.
  • Build fails after enabling cacheComponents. A route still exports runtime = "edge". Cache Components requires Node.js, so remove the export.
  • revalidate has no effect. Segment revalidation is not available on the Edge Runtime. Move the route to Node.js.
  • request.geo is undefined. It was removed in Next.js 15. Use platform headers or your host's helper package.
  • Edge route is slower than expected. It is probably calling a database or API in a single distant region. Run it on Node.js in the same region as the data.

Edge Runtime FAQ

Yes, as a route segment option. Exporting runtime as edge from a page, layout, or route handler still works but is deprecated, and Next.js recommends removing it. Proxy always runs on Node.js. The deprecated middleware filename can still run on the Edge Runtime.

Only when the work does not depend on data in a single region. Edge execution reduces cold starts and network distance to the visitor, but if the route queries a centralized database, the extra round trips usually cancel out the gain. Caching and a CDN are more effective for most sites.

Not with drivers that open TCP connections. You need an HTTP or WebSocket based serverless driver or an HTTP data API. On the Node.js runtime, standard drivers and ORMs work normally.

No. Incremental Static Regeneration, segment level revalidate, and Cache Components all require the Node.js runtime. Moving a route to Node.js is the way to cache it.

They serve the same purpose of handling requests before they reach a route. Next.js 16 renamed middleware to proxy and made Proxy run on Node.js only. The middleware filename is deprecated but still supported, and it is the only way to keep request interception on the Edge Runtime.

No. When you self-host, edge routes run inside your Node.js server in a sandbox that applies the Edge Runtime restrictions. You only get geographically distributed execution on a platform that deploys edge functions.

Conclusion

The Edge Runtime gave Next.js developers fast cold starts and global execution at the cost of a restricted API surface and missing caching features. In Next.js 16 the trade-off no longer favors it: route-level runtime = "edge" is deprecated, Proxy runs on Node.js, and Cache Components requires Node.js.

You can still opt a route into the Edge Runtime, and middleware.ts still runs there, but the better plan for most projects is to write server code against Web Standard APIs, keep it on the default Node.js runtime close to your data, and use caching and a CDN for speed. Code written that way runs anywhere, which makes the migration a matter of deleting one line per file.

Here are some useful references for going deeper on the Edge Runtime:

  1. Next.js Docs: Edge Runtime API reference — supported and unsupported APIs.
  2. Next.js Docs: runtime route segment config — the nodejs and deprecated edge values.
  3. Next.js Docs: Proxy file convention — request interception on Node.js in Next.js 16.
  4. Next.js Docs: Version 16 upgrade guide — the middleware to proxy rename and runtime changes.
  5. MDN Web Docs: Web Crypto API — the cryptography API that works on every runtime.
Tags :
Share :

Related Posts

Building Powerful Desktop Applications with Next.js

Building Powerful Desktop Applications with Next.js

Next.js is a popular React framework known for its capabilities in building server-side rendered (S

Continue Reading
Can use custom server logic with Next.js?

Can use custom server logic with Next.js?

Next.js, a popular React framework for building web applications, has gained widespread adoption for its simplicity, performance, and developer-frien

Continue Reading
TypeScript with Next.js?

TypeScript with Next.js?

Next.js has emerged as a popular React framework for building robust web applications, offering developers a powerful set of features to enhance thei

Continue Reading