Edge runtime vs Node.js runtime in Next.js
Edge runtime runs Next.js server code in V8 isolates distributed across 200+ CDN locations worldwide. Node.js runtime (the default) gives full access to the Node.js ecosystem: fs, native modules, persistent connections, and any npm package.
Theory
TL;DR
- Node.js is a full kitchen:
fs,crypto,net, native modules, full heap - Edge is a vending machine at every airport: limited tools, instant from anywhere
- Core difference: Node.js uses V8 + libuv event loop; Edge uses V8 isolates with Web APIs only
- Cold start: Edge ~40ms vs Node.js ~400ms (Vercel benchmark data)
- Decision rule: Edge for auth and redirects; Node.js for database access and heavy compute
Quick example
// Node.js runtime (default) - full API access
import fs from 'fs';
export async function GET() {
const data = fs.readFileSync('./products.json', 'utf-8');
return Response.json(JSON.parse(data)); // reads from local disk
}
// Edge runtime - Web APIs only
export const runtime = 'edge';
export async function GET() {
const res = await fetch('https://api.example.com/products');
return Response.json(await res.json()); // ~40ms at the nearest PoP
}Node.js reads from disk. Edge cannot touch the filesystem, so you fetch from a remote source instead. The tradeoff is direct: full capabilities vs global latency.
Key difference
Node.js runtime is a standard server process: V8 plus libuv's I/O loop. It opens sockets, reads files, and runs native add-ons. Edge runtime compiles your code into V8 isolates, removes everything that needs an OS-level syscall, and deploys copies to CDN points of presence globally. You lose filesystem access and Node.js built-ins. You get global distribution and cold starts near 40ms instead of 400ms.
When to use
- JWT validation, auth checks, redirects -> Edge (stateless, runs close to the user, no DB roundtrip needed)
- Geo-based routing, A/B testing -> Edge (
request.geois available natively in middleware only) - Database queries with Prisma or Drizzle -> Node.js (native drivers do not run in isolates)
- File processing, image manipulation -> Node.js (needs
fsand native bindings) - Long compute jobs (>1s) -> Node.js (Edge caps at 30s CPU and ~1MB memory)
- Cold-start-sensitive stateless paths -> Edge; persistent connection pools -> Node.js
Comparison table
| Feature | Node.js Runtime | Edge Runtime |
|---|---|---|
| APIs | Full Node.js (fs, crypto, net) | Web standards (Fetch, URLSearchParams, crypto.subtle) |
| Where it runs | Single region or self-hosted | 200+ PoPs globally (V8 isolates) |
| Cold start | ~400ms | ~40ms |
| CPU limit | 300s | 30s |
| Memory | Full heap | ~1MB |
| Filesystem | Yes (fs module) | No |
| Native modules | Yes | No |
| npm packages | All | Web API-compatible only |
| Streaming | Yes | Yes (up to 4MB in Next.js 15) |
| State | Persistent (connections reused) | Stateless only |
| Best for | DB access, file ops, heavy compute | Auth, redirects, personalization |
How it works internally
Next.js compiles Edge routes into V8 isolate bundles using esbuild. There is no libuv I/O loop involved. Any import of a Node.js built-in fails at build time (next build), not at runtime. When deployed to Vercel, Edge bundles run in a Cloudflare Workers-compatible environment that polyfills some missing Web APIs but rejects fs, path, net, and similar modules entirely.
Middleware in Next.js always runs on Edge. That is not configurable.
Common mistakes
Importing fs in an Edge route:
// Build error: "Module not found: Can't resolve 'fs'"
export const runtime = 'edge';
import fs from 'fs'; // fails at next build
// Fix: fetch from a CDN or remote API
const res = await fetch('https://my-cdn.com/data.json');
const data = await res.text();Large environment variables over 4KB:
Edge serializes env vars to request headers. Secrets over 4KB get truncated without any warning in development. The bug only appears in production.
// Truncated in production, no error thrown
const bigKey = process.env.HUGE_PRIVATE_KEY;
// Fix: use Vercel Edge Config or an external secrets API
const config = await fetch('https://edge-config.vercel.com/...');Porting Express-style handlers directly:
// Works in Node.js, returns 400 in Edge
export default (req, res) => res.json({ body: req.body });
// Fix: Edge uses the Web Request API
export async function POST(request: Request) {
const body = await request.json();
return Response.json({ body });
}Crypto API incompatibility between runtimes: Edge uses crypto.subtle (Web Crypto API). Node.js traditionally uses crypto.createCipheriv. If you write Edge crypto code with crypto.subtle and copy it to a Node.js handler, it works fine on Node.js 18+ (which ships crypto.subtle globally). The reverse is never safe: createCipheriv in an Edge route fails at build time.
Hitting the 1MB memory cap in loops: V8 isolates kill the request when memory runs out. No clear error message appears. If you process large payloads, stream them: for await (const chunk of stream) instead of buffering the full response in memory.
Real-world usage
- Vercel Speed Insights: Edge runtime for geo-based sampling across regions
- Stripe and Supabase: Edge middleware for JWT validation (no database roundtrip needed)
- Shopify Hydrogen: Edge routes for cart personalization
- Linear.app: Edge API routes for webhook fan-out
- T3 Stack (
create-t3-app): auth middleware runs on Edge by default
Follow-up questions
Q: Which Node.js APIs are missing in Edge runtime?
A: fs, path, net, zlib, child_process, and most Node built-ins. Replace them with Fetch, URLSearchParams, and crypto.subtle.
Q: How does choosing Edge affect the build process?
A: esbuild removes Node.js polyfills and fails the build if any imported module depends on a Node built-in. You find these problems at next build, not in production.
Q: What are the actual cold start numbers?
A: Vercel benchmarks put Edge at ~40ms and Node.js at ~400ms. The gap comes from isolate startup cost being near-zero vs spinning up a Node process.
Q: Any Edge limitations worth knowing in Next.js 15?
A: Streaming in Edge routes is capped at 4MB. React's cache() function does not work in Edge handlers either.
Q (senior): You want to rate-limit requests by IP using Redis in Edge middleware. What is the problem and how do you solve it?
A: Edge is stateless. It cannot hold a persistent TCP connection to Redis. The fix is Upstash Redis, which wraps Redis commands in HTTP calls so Edge can reach it via Fetch. You pay one HTTP roundtrip per request, so you need to factor that latency in and consider caching rate-limit state with TTL headers.
Examples
Edge vs Node.js for the same data route
// app/api/node/route.ts - Node.js runtime (default)
import fs from 'fs';
import path from 'path';
export async function GET() {
const filePath = path.join(process.cwd(), 'data', 'products.json');
const raw = fs.readFileSync(filePath, 'utf-8');
return Response.json(JSON.parse(raw));
// Reads from disk, runs in one region, ~400ms cold start
}// app/api/edge/route.ts - Edge runtime
export const runtime = 'edge';
export async function GET() {
const res = await fetch('https://api.example.com/products');
return Response.json(await res.json());
// No filesystem, runs at 200+ PoPs, ~40ms cold start
}Same endpoint, different constraints. Pick Node.js when data lives on the server. Pick Edge when the upstream API is remote and your users are globally distributed.
Production middleware: geo redirect and bot blocking
// middleware.ts - always runs on Edge, not configurable
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const country = request.geo?.country;
const ua = request.headers.get('user-agent') ?? '';
// Block bots in the US before they reach the origin
if (country === 'US' && ua.toLowerCase().includes('bot')) {
return NextResponse.redirect(new URL('/blocked', request.url));
}
if (country === 'UA') {
return NextResponse.rewrite(new URL('/ua', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next|favicon.ico).*)'],
};
// Pattern from vercel/commerce - blocks bots in <20ms globallyrequest.geo is Edge-only. It is not available in Node.js API routes. This middleware executes before the page renders, at the CDN layer, without touching the origin server at all.
Crypto migration between runtimes
// app/api/encrypt/route.ts - written for Edge
export const runtime = 'edge';
export async function POST(request: Request) {
// Web Crypto API - works on Edge and on Node.js 18+
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const payload = await request.arrayBuffer();
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
payload
);
return new Response(encrypted);
}
// If someone ports this to Node.js using the old API:
// import { createCipheriv, randomBytes } from 'crypto';
// That version BREAKS on Edge at build time.
// Write with crypto.subtle. It runs everywhere.Write encryption logic with crypto.subtle. It runs on Edge and on Node.js 18+. I've seen this exact problem in teams copying Stripe webhook handlers from an older Node.js service into Edge middleware: build fails because the original code used createCipheriv, which Edge has never supported.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.