EmpireUI
Get Pro
← Blog7 min read#nextjs#edge-runtime#react

Next.js Edge Runtime: When to Use It and Its Limitations

Edge Runtime in Next.js 15 promises global low latency — but it's not a drop-in for Node.js. Here's what actually works, what breaks, and when to bother.

Server rack lights in a data center representing edge computing infrastructure

What Is the Next.js Edge Runtime, Actually?

Honestly, most developers reach for Edge Runtime because it sounds fast and modern — then spend two days debugging why fs, crypto, and half their npm dependencies suddenly don't exist. Let's fix that by starting with what it actually is.

Edge Runtime in Next.js runs your code in a V8-based sandbox — not Node.js. Think of it like a browser environment with a thin server API layer bolted on. It's what Vercel's Edge Network and Cloudflare Workers use under the hood. The key word there is *sandbox*. You're trading a full Node.js environment for cold-start times under 1ms and global distribution across 300+ PoPs.

Since Next.js 13.x, you can opt a Route Handler or Middleware into Edge Runtime by exporting export const runtime = 'edge'. In Next.js 15.1+, this works for both app/ and API routes. The catch is that 'edge' doesn't mean 'the same as Node.js but faster'. It means a fundamentally different execution environment with a subset of APIs.

The runtime supports the Web API surface: fetch, Request, Response, Headers, ReadableStream, TextEncoder, URLSearchParams, crypto.subtle (Web Crypto, not Node's crypto). That's it. No Buffer, no process.env reading after build time in some configs, no native addons, no filesystem.

Where Edge Runtime Actually Makes Sense

The sweet spot is Middleware. If you're doing auth token validation, geolocation-based redirects, A/B test bucket assignment, or rate limiting at the routing layer — Edge Middleware is genuinely excellent. It runs before your page renders, globally, with sub-5ms overhead in most cases.

Another strong case is lightweight personalization. Rewriting URLs based on a cookie, injecting a country code header, redirecting authenticated users away from the login page. These are all stateless, low-compute tasks that don't need Node.js capabilities. Edge handles them better than any Lambda cold start ever will.

Streaming responses are also a solid fit. If you're building an AI chat interface and want to stream tokens from an API call, an Edge Route Handler with ReadableStream works cleanly. You get streaming without managing your own WebSocket server or dealing with Lambda execution limits cutting off long responses.

Here's the thing: if your route does any of the following — talks to a database via a native driver, reads files, uses a Node.js-specific library, or needs more than 128MB of memory — Edge Runtime is not for you. At least not yet.

The Limitations Nobody Warns You About

The 1MB bundle size limit will sneak up on you. Edge functions have a hard cap on compressed bundle size. The moment you import a library that pulls in lodash, a heavy schema validator, or an ORM like Prisma, you'll hit the wall. Prisma explicitly states it doesn't support Edge Runtime without their Accelerate proxy service.

No native Buffer. This trips up basically everyone who's done any amount of Node.js work. You can't do Buffer.from(data, 'base64'). You'll need to use TextEncoder / TextDecoder and Uint8Array instead. It's doable, just different. JWT libraries that rely on Node's crypto module also need to be swapped out for ones that use Web Crypto (jose is the standard recommendation here).

Environment variable access at runtime is limited. In Edge Middleware, process.env is available but only for variables prefixed with NEXT_PUBLIC_ or ones explicitly inlined at build time in some deployment configs. On Vercel this mostly works fine, but if you're self-hosting with a custom server, you may find certain env vars are undefined at edge execution time.

Cold starts aren't entirely gone either. They're much shorter than Node.js Lambda cold starts — typically under 50ms vs 500ms+ — but they exist. On low-traffic routes, you'll still see occasional latency spikes. The persistent myth that edge = zero cold starts comes from Cloudflare Workers marketing, not measured reality.

Edge Middleware Setup: A Practical Example

Let's look at a realistic Middleware example. This one validates a JWT using Web Crypto (no Node.js jsonwebtoken package), then redirects unauthenticated users. It also shows how to pass data downstream via request headers.

import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
};

const SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET ?? 'fallback-dev-secret'
);

export async function middleware(req: NextRequest) {
  const token = req.cookies.get('auth-token')?.value;

  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url));
  }

  try {
    const { payload } = await jwtVerify(token, SECRET);
    const res = NextResponse.next();
    // pass user id to the page without another DB round-trip
    res.headers.set('x-user-id', String(payload.sub));
    return res;
  } catch {
    // token expired or invalid
    const res = NextResponse.redirect(new URL('/login', req.url));
    res.cookies.delete('auth-token');
    return res;
  }
}

Notice jose instead of jsonwebtoken. That's intentional — jose uses Web Crypto and works in Edge Runtime. jsonwebtoken depends on Node's crypto module and will fail silently or throw a confusing error at build time. This pattern of header-passing is also handy for forwarding user context to Server Components without re-querying the database on every render.

Edge vs Node.js Runtime: Choosing Per Route

You don't have to go all-in. Next.js lets you mix runtimes at the route level. Your marketing pages can use Node.js runtime with full database access, while your /api/stream route uses Edge for low-latency streaming. This is probably the right default strategy for most apps.

To explicitly opt a route into Edge in Next.js 15, export the runtime constant at the top of your route file. If you don't export it, you get Node.js by default — which is fine. Don't assume you need Edge on every API route. The overhead of Node.js Lambda functions on Vercel is already very low for most use cases.

// app/api/stream/route.ts
export const runtime = 'edge';
export const dynamic = 'force-dynamic';

export async function GET(req: Request) {
  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder();
      const tokens = ['Hello', ' from', ' the', ' edge'];

      for (const token of tokens) {
        controller.enqueue(encoder.encode(`data: ${token}\n\n`));
        // simulate token delay
        await new Promise((r) => setTimeout(r, 80));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  });
}

One thing worth noting: if you're building something where React performance tuning matters, placing heavy computation inside an Edge function that has a 128MB memory ceiling and a strict CPU time limit can cause issues. Always benchmark with realistic payloads.

Common Errors and How to Debug Them

The most common error you'll see is something like Module not found: Can't resolve 'node:crypto' or The edge runtime does not support Node.js 'buffer' module. These usually mean a dependency somewhere in your import tree is pulling in Node.js built-ins.

Run next build with NEXT_DEBUG_EDGE=1 to get a more detailed trace of which modules are causing issues. You can also check the .next/server/edge-chunks directory after a build to inspect what got bundled. If you're using TypeScript, adding "lib": ["ES2022", "WebWorker"] to your tsconfig can catch incompatible API usage at type-check time — though it's not a complete substitute for actually running the code. Speaking of TypeScript patterns, React TypeScript tips covers some useful config setups that apply here too.

Another common gotcha: Response and Request objects from Edge routes behave slightly differently than Node.js IncomingMessage. If you've got middleware that clones a request body, remember that bodies are streams and can only be consumed once. Clone them with req.clone() before reading.

Is your bundle hitting the 1MB limit? Use @next/bundle-analyzer or just check the build output. Switching to lighter alternatives — date-fns instead of moment, superjson only where needed, tree-shaking your utility imports — usually shaves enough to stay under the cap.

Integrating Edge Routes With UI Components

If you're building a UI layer on top of Edge API routes — say, a dashboard that polls a streaming endpoint or displays real-time notifications — the component side needs some care. Edge responses arrive as streams, not plain JSON, so your client fetch logic needs to handle SSE or chunked transfer encoding properly.

For components that show live data from Edge streaming routes, you'll want to think about loading states and error boundaries carefully. A nice pattern is combining an Edge streaming route with a client component that progressively renders tokens. If you're already using a notification system in your app, React toast notifications pairs well with this — you can surface stream errors as toasts without blocking the main UI.

Themes and visual states in your UI components don't interact with Edge Runtime directly, but if you're doing server-side theme detection (checking a cookie in Middleware to set a data-theme attribute on <html>) that's actually an excellent Edge use case. You avoid a flash of incorrect theme without any client-side JavaScript. Check out theme toggle in React for patterns that work well alongside this approach.

Also worth mentioning: if your Edge route returns UI fragments or uses React Server Components streaming, make sure your Suspense boundaries are set up correctly on the client. Streaming HTML from the edge is fast, but a missing Suspense wrapper will make the whole page wait instead of progressively hydrating.

Should You Default to Edge Runtime in New Projects?

Short answer: no. Default to Node.js runtime and opt specific routes into Edge where you have a concrete reason. The temptation to use Edge everywhere because it sounds faster is real, but most apps don't have the traffic distribution or latency requirements that justify working around its limitations on every route.

Where Edge genuinely earns its place: global Middleware (auth, redirects, headers), lightweight geolocation logic, streaming AI responses, and any route where you need guaranteed sub-10ms response times worldwide. For everything else — database queries, file processing, heavy business logic, third-party SDK calls — Node.js runtime is less friction and more capable.

The ecosystem is improving fast. Prisma Accelerate, Neon's HTTP driver, and Drizzle's edge-compatible builds are making database access from Edge possible. But as of Next.js 15.x, you're still working around more constraints than you would in Node.js, and those constraints have real debugging costs.

Build for your actual requirements. If your /api/checkout route runs in 200ms on Node.js and your users are mostly in one region, Edge Runtime won't meaningfully improve their experience. If you're building a globally distributed SaaS with users across 5 continents and your auth check is in the critical path of every page load — that's where Edge earns its complexity tax.

FAQ

Can I use Prisma with Next.js Edge Runtime?

Not with the standard Prisma client. Prisma relies on Node.js native bindings that aren't available in Edge Runtime. You'd need Prisma Accelerate (their connection pooling proxy) or switch to an HTTP-based driver like Neon serverless or PlanetScale's fetch-compatible driver. Drizzle ORM has better edge support out of the box.

What's the actual memory limit for Edge Runtime functions?

128MB on Vercel's Edge Network as of 2026. CPU execution time is also limited — typically 50ms of actual CPU time per invocation (wall clock can be longer for I/O-bound tasks like fetch). If you're doing anything memory-intensive like image processing or large JSON transformations, you'll hit this ceiling.

Why does `process.env` return undefined in my Edge Middleware?

Edge Runtime inlines environment variables at build time rather than reading them at runtime like Node.js does. Make sure the variable is available during the build (set in your CI/CD environment or Vercel project settings). Variables prefixed with NEXT_PUBLIC_ are always inlined. Secret server-side vars need to be present at build time in your deployment config, not just at runtime.

Can I use `crypto.randomUUID()` in Edge Runtime?

Yes — Web Crypto's crypto.randomUUID() and crypto.subtle are available in Edge Runtime. What's not available is Node.js's crypto module (the one you'd import with import crypto from 'crypto'). Stick to the global crypto object and you're fine.

How do I debug an Edge function that fails only in production?

Run next dev --experimental-https to get closer to production Edge behavior locally. Also set NEXT_DEBUG_EDGE=1 during next build for verbose bundle output. Vercel's runtime logs (not build logs) will show Edge function errors. For local reproduction, Miniflare can emulate the V8 isolate environment more accurately than the default Next.js dev server.

Is Edge Runtime the same as Cloudflare Workers?

Same underlying technology (V8 isolates), different deployment target. When you deploy to Vercel, your Edge functions run on Vercel's infrastructure. You can also deploy Next.js to Cloudflare Pages with the @cloudflare/next-on-pages adapter, which then runs on Workers. The API surface is similar but not identical — some Web APIs available on Cloudflare aren't on Vercel and vice versa.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Next.js Middleware: Auth, Redirects, A/B Tests at the EdgeNext.js Analytics: Measuring Real User Web VitalsView Transitions API: Page Animations Without a FrameworkLighthouse CI: Automated Performance Checks in GitHub Actions