Edge Runtime vs Node Runtime in Next.js: Real Tradeoffs
When the edge runtime is worth it, when the Node runtime is the right call, and the API and library constraints that decide it for you.
Next.js lets you run route handlers and middleware on either the edge runtime or the Node runtime, and the choice is not a style preference. The two runtimes have genuinely different capabilities, cold start profiles, and constraints. Picking the wrong one shows up as either missing APIs or latency you did not expect.
What the edge runtime actually is
The edge runtime is a constrained JavaScript environment based on Web APIs, the same shape of environment you find in service workers and modern serverless edge platforms. It runs close to the user geographically, and it starts fast because the runtime is lightweight. What it gives up is the full Node.js API surface. There is no fs, no native modules, no arbitrary npm package that reaches for Node internals.
The Node runtime is the full server you already know. Every Node API is available, native modules work, and any library that assumes a Node environment runs without surprises. The cost is a heavier runtime with a different cold start and execution profile, and execution that is typically regional rather than globally distributed.
It helps to be concrete about what the edge environment actually exposes. You get fetch, Request, Response, URL, URLSearchParams, Headers, TextEncoder, crypto.subtle, structuredClone, and the streaming primitives like ReadableStream. You do not get Buffer in the way Node code expects it, you do not get process with its full surface, and you do not get the synchronous file and path modules that a surprising number of libraries quietly depend on. The environment is deliberately close to the web platform, which is what allows it to run in many places at once.
The question is rarely which runtime is faster. It is which runtime can run your code at all, and where the work needs to happen relative to the user.
The deciding constraint is usually libraries
Before you reason about latency, check what your code imports. A surprising amount of the decision is made for you by your dependencies. Database drivers that open raw TCP connections, image processing libraries with native bindings, and anything that touches the file system will not run on the edge. If your handler needs those, the Node runtime is the answer and the latency conversation is moot.
export const runtime = "edge";
export async function GET(request: Request) {
const url = new URL(request.url);
const country = request.headers.get("x-vercel-ip-country") ?? "US";
return Response.json({ country, path: url.pathname });
}
This handler uses only Web APIs, so it is a clean edge candidate. The moment you add a Node only dependency, it stops being one.
The failure here is often indirect, which is what makes it frustrating. You import a perfectly innocent looking utility package, and three levels down its dependency tree something calls require("crypto") and uses a Node only method, or reads a file at module load. The build either fails with a message about an unsupported API, or worse, it builds and then throws at runtime in production. A practical habit is to keep edge handlers on a short, audited list of dependencies, and to be suspicious of any package whose job involves the file system, native performance, or raw network sockets.
A second practical wrinkle is database access. Many traditional drivers hold a long lived pooled TCP connection, which the edge cannot do because edge invocations are short lived and globally distributed. The pattern that works at the edge is an HTTP based data API or a driver designed for serverless connection over fetch. If your data layer is a classic connection pool, that alone usually decides the question in favor of Node.
Where the edge genuinely wins
The edge shines for work that is light, latency sensitive, and benefits from running near the user. Personalization and routing in middleware is the canonical case. Reading a geolocation header, rewriting a request based on a cookie, or doing an auth check that gates a redirect are all small, fast operations where shaving the network round trip to a central region is a real win.
A second strong case is read paths that hit a globally distributed data store. If your data is replicated to the edge, doing the read at the edge keeps the whole request close to the user. Pairing an edge handler with a centralized database in a single region often backfires, because the handler is near the user but every query travels to the distant database anyway.
A third case worth naming is A/B testing and feature flag evaluation in middleware. Because middleware runs before the page renders and runs at the edge, it is the ideal place to read a cookie, decide which variant a user belongs to, and rewrite the request to the right version, all without a flash of the wrong content and without a round trip to a central service. The decision is cheap, it is pure logic over data already in the request, and it benefits directly from running close to the user.
Where Node is the right call
Heavy compute, long running work, and anything that needs the full library ecosystem belong on Node. Generating a PDF, processing an upload, talking to a database over a pooled connection, or running a library with native bindings are all Node territory. The Node runtime also gives you a more familiar debugging and observability story, which matters more than people admit when something breaks at 2 a.m.
There is also a streaming nuance. Both runtimes can stream responses, but if the work producing the stream is itself heavy or depends on Node libraries, you want Node. An AI endpoint that streams tokens from a model provider can run at the edge if it is essentially a fetch proxy with light transformation, but the moment it needs to do server side tokenization with a native library or maintain state across the stream, Node is the safer home.
Cold starts are not the whole story
It is tempting to reduce this to cold start numbers, but that misses the point. A fast cold start on the edge does you no good if every request then makes a slow query to a distant database. Conversely, a Node function in the same region as your database may produce a faster end to end response than an edge function that has to reach across the world for data. Reason about the entire request path, not one segment of it.
The cleanest way to think about it is total time as the sum of three parts: the time to start executing, the time spent in your code, and the time spent waiting on whatever your code calls. The edge wins the first part decisively and wins the last part only when the data it needs is also distributed. If your data lives in one region, the edge often trades a small win on startup for a large loss on the data round trip, and the user feels the loss.
A practical decision sequence
Walk the decision in order. First, does your code use any Node only API or library. If yes, choose Node and stop. If no, ask whether the work is latency sensitive and benefits from proximity to the user, such as middleware level routing and personalization. If yes, the edge is a strong fit. If the work is heavy or talks to a regional backend, Node is usually the better end to end choice even though it could technically run on the edge.
export const runtime = "nodejs";
export async function POST(request: Request) {
const body = await request.json();
const result = await db.order.create({ data: body });
return Response.json(result, { status: 201 });
}
This write path uses a pooled database connection and belongs on Node, full stop.
One more edge case deserves a mention: mixed needs within a single feature. It is entirely reasonable to run middleware at the edge for fast routing and personalization, while the route handlers it forwards to run on Node because they touch the database. The runtime is chosen per file, so you are not forced into one answer for the whole application. The strongest architectures use the edge for the thin, latency sensitive perimeter and Node for the substantive work behind it.
Practical takeaways
- Decide by constraints first and latency second. If any dependency needs a Node API, the choice is already made.
- Audit the full dependency tree of edge handlers. The breaking import is usually three levels deep and touches the file system, native bindings, or raw sockets.
- The edge wins for light, latency sensitive, globally distributed work: middleware routing, personalization, geolocation, feature flags, and reads against replicated data.
- Node wins for heavy compute, pooled database connections, native libraries, long running work, and anything that needs the full HTTP and runtime surface.
- Reason about the whole request path. A fast edge start paired with a distant single region database is usually slower end to end than a Node function colocated with that database.
- Mix runtimes deliberately. Run the thin perimeter at the edge and the substantive work on Node, choosing per file rather than per application.
The honest summary
The edge runtime is a sharp tool for light, latency sensitive, globally distributed work, and it is the wrong tool the moment you need a Node API or you are talking to a single region backend. The Node runtime is the dependable default for everything heavy or library dependent. Choose by constraints first and latency second, and the decision stops feeling like guesswork.