open atlas
↑ Back to track
Next.js, zero to senior NEXT · 03 · 03

Node vs Edge runtime: cold starts, missing APIs, and why compute must live near data

Edge runtime trades Node APIs (native modules, fs, long CPU) for near-zero cold starts and natural streaming. The catch is placement: edge compute next to the user with a central DB pays cross-region RTT per query. Choose runtime and region per route — code belongs near its data.

NEXT Senior ◷ 17 min
Level
FoundationsJuniorMiddleSenior

A startup reads the pitch — “deploy globally, run at the edge, milliseconds from every user” — and adds export const runtime = 'edge' to their API routes. Their users are worldwide; their Postgres is in Frankfurt. A user in Singapore hits /api/dashboard. The edge function spins up 30 kilometers away — beautifully, in under 5ms, no cold start. Then it runs its first query: 160ms round trip to Frankfurt. Then the second query, which needed the first’s result: another 160ms. Then the ORM’s lazy-loaded relation: 160ms more. The response that took 90ms total from a boring Node server in Frankfurt now takes 500ms+ from the edge — the function ran next to the user and far from the data, and it paid intercontinental RTT for every round trip in a chatty conversation. They rolled back to the Node runtime in one region and latency dropped by two thirds. Edge isn’t faster; edge is closer — and closer to the user is only a win when the work doesn’t keep calling home.

Two runtimes, one line of config

When you pick a runtime, you’re not choosing a feature flag — you’re choosing a different set of physical constraints that will determine where your function can run, what packages it can load, and how expensive every database round trip becomes. The wrong choice doesn’t error in development; it costs you seconds of latency in production.

Next.js can execute server code in two environments, chosen per route segment with one export. The Node.js runtime (the default) is a full Node process: every npm package works, native addons load, fs and child_process exist, CPU can crunch for as long as your platform allows. The Edge runtime is a stripped V8 isolate modeled on web standards — the same execution model as Cloudflare Workers and Vercel Edge Functions — exposing fetch, Request/Response, URL, web crypto, streams… and almost nothing else.

// app/api/geo/route.ts — opt this one route into the edge runtime
export const runtime = "edge";        // default: "nodejs"

export async function GET(request: Request) {
  // web-standard APIs only: fetch, crypto.subtle, ReadableStream…
  const upstream = await fetch("https://api.example.com/snapshot");
  return new Response(upstream.body, {
    headers: { "content-type": "application/json" },
  });
}

The differences are not features versus fewer features — they are different physics. A Node serverless function is a process (or microVM) that must boot: on a cold start you pay runtime initialization plus your bundle’s import graph, typically hundreds of milliseconds to over a second for a fat bundle with an ORM. A V8 isolate is closer to opening a new tab in an already-running browser: single-digit milliseconds, often indistinguishable from warm. That asymmetry is the entire honest case for edge — not raw speed (per-instruction, the same V8 executes your JavaScript), but the elimination of cold-start tax and the ability to run in many points of presence at once.

What edge cannot do — and how you find out

The constraint list is long and you usually discover it via a build error. No native modules: anything with a .node binary — bcrypt, sharp, most database drivers’ fast paths — won’t load. No fs: there is no filesystem; templates, fonts, anything you read at runtime must be bundled or fetched. No raw TCP: classic Postgres/MySQL/Redis drivers can’t open sockets, which is why the edge ecosystem leans on HTTP/WebSocket-proxied databases (Neon, PlanetScale, Upstash) — an extra protocol hop you should count in your latency budget. Capped CPU: platforms budget edge CPU in tens of milliseconds (Cloudflare’s free tier: 10ms CPU; paid: 50ms+); rendering a 2MB PDF or bcrypt-hashing at cost factor 12 simply doesn’t fit. Bundle size limits: typically 1–4MB compressed, which a single heavyweight dependency can blow.

The subtle trap is the dependency you didn’t pick: your auth library pulls a JWT package that pulls a Node crypto shim, and the build fails — or worse, a polyfill silently swaps in a slow pure-JS path. Treat runtime = 'edge' as a contract your whole import graph must satisfy, not a flag you set on a file. Together, these constraints mean the edge runtime is genuinely appropriate for a narrow slice of routes: without native modules, filesystem, or TCP, you can’t run a database-backed handler there — but for pure compute, request shaping, and streaming proxies, the near-zero cold start changes everything.

Quiz

A team moves a /api/avatar route to runtime = 'edge'. It resizes uploaded images with sharp and writes a temp file before uploading to S3. The deploy fails. Which pair of edge constraints did they hit?

Cold start economics and streaming

Put numbers on the asymmetry. A Node lambda cold start: 200–800ms for runtime boot plus bundle evaluation (an ORM with generated clients can dominate this), paid by an unlucky subset of requests — exactly the tail that ruins your p99. Mitigations exist (provisioned concurrency, bundle dieting, in-function lazy imports) but they cost money or discipline. An isolate cold start: ~5ms, paid by almost nobody. If your traffic is spiky — webhooks, cron fan-out, morning login storms — the edge model removes an entire class of tail latency. If your traffic is steady, warm Node functions and the difference mostly disappears.

Streaming is the second genuine edge strength: isolates are built around web streams, so returning a ReadableStream — proxying an LLM API token-by-token, transforming an upstream response on the fly — is the natural mode, with time-to-first-byte in the tens of milliseconds from a nearby PoP. Platforms cap edge CPU, not wall-clock waiting: a function that spends 30 seconds awaiting an upstream model while burning 40ms of CPU fits comfortably. This combination — instant start, cheap waiting, native streams — is why “AI gateway” routes are the strongest remaining edge use case.

Pick the best fit

A Next.js app has two new routes: (A) /api/geo — reads the Accept-Language header and returns a JSON locale hint with no database queries; (B) /api/feed — runs 3 sequential Postgres queries (auth, entitlements, data) against a DB in eu-west-1 and returns a personalised feed. Users are worldwide. Which runtime for each?

Placement: the data gravity rule

Now the Hook’s trap, generalized. Edge platforms run your function in the PoP nearest the user. Your database lives in one region. Every query is a round trip from function to database, and a typical request makes several — sequentially, when each result feeds the next (auth lookup → entitlements → actual data). The arithmetic is brutal: Singapore→Frankfurt RTT is ~160ms, so three sequential queries cost ~480ms before any compute, versus ~3ms for the same three queries from a Node function in the same datacenter as Postgres — the user then pays the 160ms once, on the final response. Chatty-with-data code must live near the data; the user-facing hop should be the only long one.

So choose per route, not per app. Routes that touch the primary database: Node runtime, deployed in the database’s region. Routes that are pure compute-on-request, talk only to globally replicated stores (KV, CDN caches, replicated read layers), or proxy streams: edge candidates. And note the industry correction: Next.js and Vercel themselves walked back “edge-first” messaging — Vercel’s own guidance moved to regional Node functions as the default story precisely because of this data-gravity math. The edge pitch survives for what it was always good at: middleware-class request shaping, streaming proxies, and serving users whose data is also distributed.

Quiz

Users in Sydney complain a dashboard API is slow. It runs at the edge (PoP in Sydney) and makes 4 sequential Postgres queries to us-east-1 (~200ms RTT each). What is the dominant cost, and the highest-leverage fix?

Recall before you leave
  1. 01
    What exactly does the edge runtime give up relative to Node, and how do the constraints surface?
  2. 02
    State the data gravity rule with the numbers that justify it, and the per-route decision it implies.
Recap

Next.js picks an execution environment per route segment via one export: the default Node.js runtime — a full process where every package, native addon, filesystem call, and long computation works — or the Edge runtime, a V8 isolate exposing only web-standard APIs, the execution model of Cloudflare Workers and Vercel Edge Functions. The physics differ more than the feature lists: a Node lambda cold start costs 200–800ms of runtime boot plus bundle evaluation and lands on your p99, while an isolate starts in single-digit milliseconds — and platforms cap edge CPU (tens of milliseconds) but not wall-clock waiting, which makes the edge superb at streaming: ReadableStream proxying of LLM APIs with instant time-to-first-byte is its strongest modern use case. The give-ups are concrete: no native modules, no fs, no raw TCP (so classic database drivers fail and the ecosystem substitutes HTTP-proxied databases), tight bundle caps, and the trap that a transitive dependency can violate the contract for you — discovered at build time if you’re lucky. The deciding factor for real systems is data gravity: edge compute runs near the user, but the database lives in one region, and every sequential query pays the full function-to-database round trip — at Sydney-to-us-east distances, four sequential queries cost ~800ms from the edge versus ~4ms from a Node function in the database’s datacenter, after which the user pays the intercontinental hop exactly once. So the choice is per route: data-chatty routes run on Node near the database; pure compute, replicated-store reads, and streaming proxies can earn their place at the edge — a correction the framework’s own vendors made when edge-first guidance met the RTT arithmetic. Now when you see a route that’s slow despite running at an edge PoP near the user, count how many sequential database queries it makes — every one pays full cross-region RTT, and moving the function next to the database typically cuts response time by a factor of five or more.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.