|$ curl https://forge-ai.dev/api/markdown?path=docs/nextjs/edge-runtime
$cat docs/edge-runtime-&-runtime-differences-in-next.js.md
updated This weekยท25 min readยทpublished

Edge Runtime & Runtime Differences in Next.js

โ—†Next.jsโ—†Edgeโ—†Runtimeโ—†Intermediate to Advanced๐ŸŽฏFree Tools
Introduction

Next.js is not a single runtime. The same application can run code in the Node.js runtime, in the Edge runtime, and in the browser, depending on where the code is located and how it is configured. Understanding these boundaries is critical for production systems because the wrong choice can lead to broken APIs, slow responses, or security vulnerabilities.

The Edge runtime is a lightweight, Web-API-based execution environment designed to run code close to the user. It is used by middleware and can be opted into for route handlers and some API routes. It offers extremely low cold-start latency and geographic distribution, but it comes with restrictions: no Node.js file system, no native modules, and a smaller standard library.

The Node.js runtime is the default for most Next.js code. It has full access to the Node.js standard library, native packages, database drivers, and long-running processes. Server Components, most route handlers, and build-time logic run here. It is more powerful but typically runs in a smaller set of regions and can have higher cold-start latency for serverless deployments.

This guide explains the differences between these runtimes, what runs where by default, how to configure the runtime for a route handler, which APIs are available, and when to choose Edge over Node. The goal is to build an accurate mental model so you can place each piece of logic on the right runtime.

Runtime Overview

Next.js supports three distinct execution contexts: the Node.js server runtime, the Edge runtime, and the client browser. Each has its own capabilities, constraints, and startup characteristics. Knowing which context your code executes in is the first step toward debugging runtime errors and optimizing performance.

The Node.js runtime is the default for App Router route handlers, Server Components, and Server Actions. It provides full access to fs, path, crypto, the http module, and any npm package that depends on native bindings. If your code imports a package that uses Node.js internals, it must run in the Node.js runtime.

The Edge runtime is built on top of standard Web APIs such as Fetch, Request, Response, URL, crypto, and Web Streams. It is designed for small, fast, request-response handlers. Middleware always runs in the Edge runtime, and route handlers can opt in by exporting runtime = "edge".

The client runtime is the browser. Client Components, event handlers, and hooks like useState and useEffect run here. Client code has access to the DOM, browser APIs, and cannot directly read server-side secrets or databases. Always treat the client as untrusted.

RuntimeDefault ForCan Opt InKey APIs
Node.jsServer Components, route handlers, Server ActionsYes, by defaultfs, path, net, child_process, all npm packages
EdgeMiddlewareRoute handlers, API routesFetch, Request, Response, URL, Web Streams, crypto
BrowserClient Components, hydrationNoDOM, fetch, localStorage, sessionStorage

Next.js also exposes experimental runtime features and configuration flags. The export const runtime value in a route handler or page file tells Next.js which runtime to use for that segment. In Next.js 16, the valid values are "nodejs" and "edge", with Node.js being the default when the export is omitted.

๐Ÿ“

note

The Edge runtime is not just a faster Node.js. It is a different environment with a different API surface. Code that runs in Node.js may fail in Edge if it uses Node-only APIs or native modules.
What Runs Where

Not every part of a Next.js application can choose its runtime. Some layers are pinned to a specific environment because of their role in the request lifecycle. Understanding these defaults prevents you from trying to use Node.js APIs in middleware or expecting Web Streams behavior in a Node.js Server Component.

Middleware always runs on the Edge. The middleware.ts file at the project root executes before a route is matched. It is geographically distributed and must be extremely fast because it runs on every request that matches its matcher. This is why middleware cannot access the file system, raw TCP, or most npm packages. It is limited to request inspection, cookie manipulation, redirects, rewrites, and setting headers.

Route handlers can choose. A route handler in app/api/.../route.ts can export runtime = "edge" or leave the default Node.js runtime. This is the most common place to make an explicit runtime decision. Use Edge for lightweight, latency-sensitive endpoints such as geolocation, A/B testing, or auth checks. Use Node.js for endpoints that need database drivers, file parsing, or heavy computation.

Server Components default to Node.js. React Server Components in the App Router are rendered in the Node.js runtime by default. They can fetch data, read cookies, and render HTML on the server. They do not run in the Edge runtime unless you explicitly configure a route segment to use Edge, which is uncommon for pages because Server Components often need database access.

Client Components always run in the browser. Anything marked with "use client"is hydrated and executed in the user's browser. It has no access to server secrets, no direct database access, and no ability to choose a runtime. Its job is interactivity and rendering state that lives on the client.

LayerDefault RuntimeCan Change?Typical Work
MiddlewareEdgeNoAuth redirect, geo, A/B test, header injection
Route HandlerNode.jsYesAPI endpoints, webhooks, proxying
Server ComponentNode.jsYes, rarelyData fetching, rendering, metadata
Server ActionNode.jsYes, rarelyForm submission, mutation, cache revalidation
Client ComponentBrowserNoUI interactivity, browser APIs
โ„น

info

Place latency-sensitive, stateless logic in middleware or Edge route handlers. Place data-heavy, stateful logic in Node.js Server Components or route handlers.
Node API Compatibility

The biggest practical difference between Node.js and Edge is the set of APIs available to your code. The Edge runtime is intentionally minimal. It provides the Web APIs that are common across browsers and edge networks, but it omits many Node.js-specific modules that are not part of the WinterCG standard or the Vercel Edge runtime contract.

Modules that are not available in the Edge runtime include fs, path, os, child_process, cluster, net, tls, dgram, and http in its Node.js form. Any package that depends on these will fail to import or throw at runtime. This includes many traditional database drivers, ORM query engines, image processors, and PDF generators.

Modules that are available in the Edge runtime include crypto (Web Crypto API), fetch, Request, Response, Headers, URL, URLSearchParams, TextEncoder, TextDecoder, Web Streams, and structuredClone. These are the same APIs you use in a browser service worker, which makes Edge code portable and predictable.

edge-crypto/route.ts
TSX
1// Edge-compatible route handler using Web Crypto
2export const runtime = "edge";
3
4export async function GET(request: Request) {
5 const url = new URL(request.url);
6 const token = url.searchParams.get("token");
7
8 if (!token) {
9 return new Response("Missing token", { status: 400 });
10 }
11
12 const encoder = new TextEncoder();
13 const data = encoder.encode(token);
14 const key = await crypto.subtle.importKey(
15 "raw",
16 encoder.encode("secret-key-at-least-32-characters"),
17 { name: "HMAC", hash: "SHA-256" },
18 false,
19 ["sign", "verify"]
20 );
21
22 const valid = await crypto.subtle.verify("HMAC", key, data, data);
23
24 return new Response(JSON.stringify({ valid }), {
25 headers: { "Content-Type": "application/json" },
26 });
27}

The Edge runtime also supports a subset of the Node.js compatibility layer on some platforms, but you should not rely on it. Writing to the lowest common denominator of Web APIs keeps your code portable across hosting providers and avoids subtle runtime failures when an API is polyfilled differently.

API / ModuleNode.jsEdgeNotes
fs, path, osAvailableNot availableNo file system access in Edge
net, tls, dgramAvailableNot availableNo raw TCP or UDP
child_process, clusterAvailableNot availableNo process spawning
fetch, Request, ResponseAvailableAvailableStandard Web API
crypto.subtleAvailableAvailableWeb Crypto, not Node crypto
Web StreamsAvailableAvailableReadableStream, WritableStream
process.envAvailableAvailableLimited to build/runtime env vars
BufferAvailableLimitedPrefer Uint8Array and TextEncoder
โš 

warning

Do not assume a package works in Edge because it works in Node.js. Check the package for Node-only imports such as fs, net, or tls. Native bindings and WebAssembly modules are also common failure points.
Configuring Runtime

Runtime selection in Next.js is done at the route segment level. You export a constant from the route handler or page file, and Next.js uses that to determine which runtime to load. This is a declarative model: the runtime is part of the route's static configuration, not a runtime decision.

To opt a route handler into the Edge runtime, add export const runtime = "edge" at the top of the file. This tells Next.js to bundle the handler for the Edge environment and run it in the closest edge region to the user. If you omit the export, the route defaults to the Node.js runtime.

route.ts
TSX
1// app/api/edge-time/route.ts
2export const runtime = "edge";
3
4export async function GET() {
5 return new Response(JSON.stringify({ now: Date.now() }), {
6 headers: { "Content-Type": "application/json" },
7 });
8}

You can also configure the preferredRegion export to hint where the function should run. This is especially useful for Node.js serverless functions that need to be close to a database or a third-party API. For Edge functions, the region is typically distributed automatically, but the flag can still influence deployment behavior on some platforms.

node-region/route.ts
TSX
1// Run this Node.js handler in a specific region
2export const runtime = "nodejs";
3export const preferredRegion = "iad1"; // US East (N. Virginia)
4
5export async function GET() {
6 const data = await fetchFromDatabaseInSameRegion();
7 return Response.json(data);
8}

The next.config.ts file can also influence runtime behavior. You can set headers, redirects, and rewrites that apply globally, and some experimental flags affect how middleware and edge functions are bundled. However, the runtime export is the most direct way to control a single route.

next.config.ts
TSX
1// next.config.ts
2import type { NextConfig } from "next";
3
4const nextConfig: NextConfig = {
5 // Optional: configure default headers for edge and node routes
6 async headers() {
7 return [
8 {
9 source: "/api/:path*",
10 headers: [
11 { key: "X-Content-Type-Options", value: "nosniff" },
12 { key: "X-Frame-Options", value: "DENY" },
13 ],
14 },
15 ];
16 },
17};
18
19export default nextConfig;

You can set a runtime at the layout level for an entire route segment, but this applies to Server Components and route handlers inside that segment. Middleware is unaffected by these exports because it is always Edge. Client Components are unaffected because they run in the browser.

โœ“

best practice

Keep runtime exports at the top of the file, immediately after imports. Do not make the runtime value conditional or derive it from environment variables at runtime. Next.js needs to know the runtime at build time to bundle the route correctly.
Cold Start & Latency

One of the main reasons to use the Edge runtime is latency. Edge functions are designed to start quickly and run close to the user. A cold start for an Edge function is typically measured in single-digit milliseconds, whereas a Node.js serverless function can take tens or hundreds of milliseconds to initialize, especially if it imports a large dependency tree or connects to a database.

Edge functions are distributed across a larger number of regions than Node.js functions. When a request arrives, the platform routes it to the nearest edge location. This is ideal for personalization, geolocation, and redirects where the response must be fast and the logic is small. The trade-off is that Edge functions have smaller memory limits and shorter execution durations than Node.js functions.

Node.js functions are better suited for workloads that need more memory, longer timeouts, or complex dependencies. If your handler parses a large CSV, generates a PDF, or runs an ORM query, the Node.js runtime is almost always the right choice. The cold start cost is paid once, and the function can do more work before hitting limits.

FactorEdge RuntimeNode.js Runtime
Cold startVery low, ~1-5 msHigher, ~50-500 ms
DistributionMany regions, near userFewer regions, configurable
Memory limitLower, platform dependentHigher, platform dependent
Execution timeoutShorterLonger
Best forFast guards, redirects, small JSONHeavy compute, DB access, file I/O

Regional placement also matters for database-backed workloads. If your database is in a single region, running an Edge function in a distant region may add more network latency than it saves in cold start. In those cases, co-locate the Node.js function with the database using preferredRegion and accept the slightly higher cold start.

โ„น

info

Measure real latency before deciding. Edge is not always faster. A Node.js function next to your database can beat an Edge function that talks to a remote database across continents.
Use-Case Decision Matrix

Choosing between Edge and Node.js should be driven by the workload, not by preference. A good rule of thumb is: if the work is small, stateless, and latency-sensitive, use Edge. If the work is heavy, stateful, or needs Node.js APIs, use Node.js.

Auth checks are a classic Edge use case. Middleware can verify a session token, check a cookie, or redirect unauthenticated users before the request reaches a Server Component. The check is fast and runs at the edge. However, if the auth check needs to hit a database to validate a session or read user permissions, it may be better to do that in a Node.js route handler or Server Component.

Geolocationis another natural fit for Edge. The platform can expose the user's country, region, or city through request headers, and an Edge handler can redirect, localize, or block based on that information. This is hard to do as efficiently in a Node.js function that runs in a single region.

A/B testing and feature flagging can run in Edge or middleware. When the decision is based on a cookie, request header, or a deterministic hash, Edge is ideal. If the decision requires a complex rules engine or a database lookup, Node.js is safer and more flexible.

Heavy computation belongs in Node.js. Image resizing, video transcoding, PDF generation, machine learning inference, and large data transformations need more memory and CPU than Edge functions provide. Attempting to run these in Edge will hit limits or produce poor performance.

File system access is impossible in Edge. Any workload that reads from disk, writes temporary files, or uses fs must run in Node.js. Similarly, long-running tasks such as background jobs, streaming processing, or WebSocket servers are better suited to Node.js or dedicated worker infrastructure.

Use CaseRecommended RuntimeReason
Auth token validationEdgeFast, stateless, runs before route match
Session database lookupNode.jsNeeds database driver and longer timeout
Geolocation redirectEdgeUses edge headers, low latency
A/B test splitEdgeCookie-based, deterministic
Heavy computationNode.jsNeeds CPU and memory
File upload / parsingNode.jsNeeds streams, buffers, and possibly disk
Database queryNode.jsNeeds TCP driver or ORM
HTTP-based DB queryEdgeDriver uses fetch over HTTP
โœ“

best practice

Start with Node.js for anything that touches a database or the file system. Only move logic to Edge after you have confirmed it is stateless, lightweight, and compatible with Web APIs.
Edge Database Drivers

Traditional database drivers use TCP connections, which are not available in the Edge runtime. To query a database from Edge, you need an HTTP-based driver. These drivers speak the database protocol over HTTPS or WebSockets rather than raw TCP, making them compatible with Edge constraints.

Neon provides a serverless driver that uses HTTP and WebSockets. The @neondatabase/serverlesspackage can run in Edge environments and connects to Neon's Postgres API without a long-lived TCP connection. This makes it a popular choice for Edge route handlers that need to read or write small amounts of data.

PlanetScale offers the @planetscale/database driver, which is built on fetch. It is designed for serverless and edge environments and avoids the need for a persistent connection pool. Queries are sent as HTTP requests, which aligns well with the Edge runtime model.

Turso uses libSQL, a SQLite fork designed for the edge. The @libsql/client Web version communicates over HTTP and is suitable for Edge functions. It is particularly useful for applications that need a lightweight, globally replicated database with low latency reads.

edge-db/route.ts
TSX
1// Edge route handler with Neon serverless driver
2import { neon } from "@neondatabase/serverless";
3
4export const runtime = "edge";
5
6const sql = neon(process.env.DATABASE_URL!);
7
8export async function GET() {
9 const rows = await sql`SELECT id, name FROM users LIMIT 10`;
10 return Response.json(rows);
11}

The main limitation of Edge database drivers is connection pooling. Because each Edge invocation is short-lived and may run in a different region, traditional connection pooling does not apply. Every query may open a new connection or HTTP session. For high-throughput applications, this can be more expensive than a Node.js function with a persistent pool.

Another limitation is transaction-heavy workloads. Edge drivers are generally fine for simple reads and writes, but long transactions, prepared statements, and complex session state are easier to manage in Node.js. If your route needs to run multiple queries in a transaction, consider whether Node.js is a better fit.

DatabaseEdge DriverTransportBest For
Neon@neondatabase/serverlessHTTP / WebSocketPostgres queries from Edge
PlanetScale@planetscale/databaseHTTPMySQL-compatible serverless queries
Turso@libsql/client/webHTTPEdge SQLite reads
Supabase@supabase/supabase-jsHTTPPostgres via REST or realtime
Upstash Redis@upstash/redisHTTPEdge caching, rate limiting
โš 

warning

Not all ORMs work in Edge. Prisma, for example, traditionally requires a query engine binary that runs in Node.js. Use Prisma Accelerate or an HTTP-compatible client if you need ORM features from Edge.
Security Boundaries

The Edge runtime is isolated by design. Each invocation runs in a fresh, ephemeral environment. This is a security feature: there is no shared state between requests, no file system to persist data, and no raw network access to escape the sandbox. However, it also means you must not rely on in-memory state or local files between requests.

Because Edge functions are stateless, you should treat every invocation as independent. Do not store sensitive data in module-level variables and do not assume that a global cache will be available on the next request. If you need to share state, use an external store such as Redis, DynamoDB, or a database.

The lack of file system access means that any code that writes to /tmp or reads from disk will fail. This includes many libraries that use the file system for caching, configuration, or temporary storage. If you need to process files, use streaming directly in memory or move the work to Node.js.

No raw TCP or UDP means that traditional database connections, SMTP servers, and custom socket protocols are unavailable. All outbound communication must go through fetch or WebSocket APIs provided by the platform. This is why Edge-compatible database drivers use HTTP.

edge-safe/route.ts
TSX
1// Edge-safe: no secrets in globals, use env vars
2export const runtime = "edge";
3
4export async function GET(request: Request) {
5 const apiKey = process.env.EXTERNAL_API_KEY;
6 if (!apiKey) {
7 return new Response("API key not configured", { status: 500 });
8 }
9
10 const res = await fetch("https://api.example.com/data", {
11 headers: { Authorization: `Bearer ${apiKey}` },
12 });
13
14 if (!res.ok) {
15 return new Response("Upstream error", { status: res.status });
16 }
17
18 return new Response(res.body, {
19 headers: { "Content-Type": res.headers.get("Content-Type") || "application/json" },
20 });
21}

Environment variables in Edge are available at runtime, but the platform controls how they are injected. Never commit secrets to source control, and never expose them in client bundles. Edge handlers can read server-side environment variables because they run on the server, even though they are lightweight.

โœ•

danger

Edge isolation does not make unsafe code safe. Validate all inputs, sanitize data, and use the principle of least privilege when calling external APIs. A misconfigured Edge handler can still leak data or be abused.
Best Practices

1. Keep Edge handlers small. The ideal Edge function does one thing: validate a token, set a header, or redirect. If it grows beyond a few hundred lines, consider moving it to Node.js.

2. Avoid heavy dependencies in Edge. Large npm packages increase bundle size and cold start. Use only Web-API-compatible libraries.

3. Prefer Web APIs over Node.js APIs. Write code that uses fetch, URL, crypto.subtle, and Web Streams so it is portable across runtimes.

4. Test on both runtimes. A route that works in Node.js may fail in Edge. Run builds and integration tests for both configurations.

5. Use preferredRegion to co-locate Node.js functions with databases. Network latency between regions often matters more than cold start.

6. Do not run heavy computation in Edge. Image processing, PDF generation, and machine learning belong in Node.js or dedicated workers.

7. Choose Edge-compatible database drivers when querying from Edge. Verify that the driver uses HTTP, not TCP.

8. Keep middleware fast. Middleware runs on every matching request. Expensive work here slows down every page and API call.

9. Do not rely on in-memory state in Edge. Each request is isolated. Use external stores for shared state.

10. Monitor runtime errors separately. Edge failures often have different causes than Node.js failures, such as missing APIs or module import issues.

๐Ÿ”ฅ

pro tip

When you are unsure which runtime to use, default to Node.js. Only move to Edge once you have proven the workload is compatible, stateless, and benefits from lower latency or wider distribution.
$Blueprint โ€” Engineering DocumentationยทSection ID: NEXT-EDGEยทRevision: 1.0