DEV Community

Nainik Mehta
Nainik Mehta

Posted on

Next.js Edge Runtime: Edge vs Node — Practical Guide

Introduction

Edge adoption in Next.js apps is booming, but treating the Edge runtime as "Node in a smaller box" leads to late-stage surprises. This practical guide — distilled from production experience and recent platform changes — gives a one-page checklist for per-route decisions, reality-check numbers, three safe patterns to bridge runtimes, and a concrete example you can copy.

Quick decision checklist

Pick Edge when:

  • The work is stateless and latency-sensitive (auth gating, A/B routing, personalization decisions under ~50ms).
  • You can use HTTP-friendly drivers or remote APIs (Neon Serverless, PlanetScale HTTP, Turso/libSQL, Upstash) instead of raw TCP connection pools.
  • The payload and code fit edge limits (small bundles, no native modules, and simple CPU work).

Keep Node when:

  • You need DB writes, connection pooling, native libraries (sharp, bcrypt), long-running CPU tasks, or file/child_process access.
  • You generate images, PDFs, or run heavy CPU-bound transforms.

These are not ideological choices — they are pragmatic trade-offs based on APIs, latency, and platform constraints.

Reality checks and numbers

  • Cold/warm latency: many benchmarks show Edge P50s near ~100ms on cold paths versus older serverless Node P50s around ~860ms. Modern improvements like Vercel's Fluid Compute have dramatically reduced Node cold-starts (typical gaps now 30–80ms for small bundles), but the geographic advantage of running in a nearby PoP still favors Edge for global latency.

  • Caching wins: the biggest wins for high-traffic apps often come from edge-side caching (for example, Vercel Edge KV or Cloudflare KV). Bypassing origin compute with a cache can yield sub-10ms P99s.

  • DB patterns: Edge environments cannot open raw TCP sockets reliably. Use HTTP or WebSocket-based drivers (Neon Serverless with WebSocket/HTTP mode, PlanetScale HTTP client, Turso/libSQL web entry) for DB access from Edge — but expect per-request overhead. If your request does multi-statement transactions or relies on prepared statements, Node with a pooled TCP client + PgBouncer usually wins.

Constraints of the Edge runtime you must respect

  • No fs, no node:net, no native modules. Many npm packages will fail to build or run in Edge. Expect cryptic deploy-time errors (missing 'net' or 'fs').
  • Bundle size limits (Vercel: 1–4 MB for Edge bundles depending on plan) — large imports can block deploys.
  • No persistent TCP connections across invocations. Each isolate can cache objects while warm, but isolates are independent.
  • Time budgets: Edge must begin sending a response within a platform-specific start budget (e.g., Vercel historically enforces a ~25s start-of-response budget and caps streaming durations); Fluid Compute extends Node durations.

Three safe patterns to avoid surprises

1) Edge as gate + internal Node worker

Let Edge make the low-latency decision (auth accept/reject, experiment bucket, small KV lookup) and delegate heavy work to an authenticated internal Node API that runs near your DB. This keeps the UX snappy while preserving full capabilities in Node.

2) Queue background work

If a request triggers heavy enrichment, writes, or third-party calls, push a message to a durable queue (e.g., Upstash QStash, SQS, or a webhooks-backed worker) and return immediately from the Edge route. Process the job in the background on Node workers.

3) Stream from Edge, compute in Node

Use Edge to start streaming UX content and call Node for DB or heavy transforms. Send partial responses or placeholders while Node completes the heavy lifting and notifies the client when results are ready.

Concrete example: split personalization + checkout

We moved a personalization banner decision to Edge for a sub-50ms user experience, while keeping checkout (DB writes) on Node. The Edge route checks cookies, reads a tiny KV lookup, and posts enrichment data to an internal worker.

Example: edge route (app/route.js or middleware)

// app/personalize/route.js
export const runtime = 'edge';

export async function GET(req) {
  const cookie = req.headers.get('cookie') || '';
  const userId = extractUserIdFromCookie(cookie);

  // Fast KV lookup (Upstash/Vercel Edge KV) or small HTTP call
  const kvResp = await fetch(`https://edge-kv.example.com/get?key=bucket:${userId}`);
  const bucket = await kvResp.text();

  // Delegate enrichment to internal Node worker (no secrets leaked)
  // Authenticate using an internal token stored as an environment variable
  await fetch('https://your-app.com/api/worker', {
    method: 'POST',
    headers: { 'content-type': 'application/json', 'x-internal-secret': process.env.INTERNAL_SECRET },
    body: JSON.stringify({ userId, bucket })
  });

  // Return a small personalization hint for the client
  return new Response(JSON.stringify({ bucket }), { headers: { 'content-type': 'application/json' } });
}
Enter fullscreen mode Exit fullscreen mode

Worker route on Node (app/api/worker/route.js)

export const runtime = 'nodejs';

export async function POST(req) {
  const { userId, bucket } = await req.json();
  // Validate internal header (platform-level secret)
  // Do DB writes / heavy enrichment / call Prisma or sharp here
  await enrichUserProfile(userId, bucket);
  return new Response(null, { status: 202 });
}
Enter fullscreen mode Exit fullscreen mode

That simple POST delegation is a safe pattern: the Edge route stays fast and stateless while Node owns the heavy capabilities.

Practical migration rules of thumb

  • Default to Node for anything that writes to your primary DB, uses native modules, or runs longer than a few hundred milliseconds of CPU work.
  • Default to Edge for every per-request gate that must run in the user's region (auth checks, A/B assignment, geolocation) and where you can use an HTTP-friendly data layer.
  • Measure early: add benchmarks for P50/P95 cold and warm latencies for the route in both runtimes. Don’t guess.
  • Use edge caching aggressively: cache personalization keys or computed flags in Edge KV with short TTLs to get sub-10ms P99s.

Final thoughts

Next.js apps benefit most from mixing runtimes per-route: middleware and latency-sensitive gates at the Edge, database-heavy workflows in Node. The three patterns (Edge gate + Node worker, queued background jobs, and streaming hybrid) give you predictable behavior and let teams avoid the most common runtime traps.

If you document "Next.js edge runtime best practices" internally, include this checklist and the delegation patterns. They’ll save you from late-stage surprises — especially when self-hosting or designing in-route handlers.

What was the nastiest runtime surprise you hit in production? Share it and how you fixed it — the best hard-earned lessons are the ones we swap with other teams.

Top comments (0)