Every route segment in Next.js runs in one of two runtimes:
1// app/api/echo/route.ts
2export const runtime = "edge"; // or "nodejs" (default)Same option on a page:
1// app/page.tsx
2export const runtime = "edge";| Use case | Why Edge wins |
|---|---|
| Auth middleware | Run at the CDN edge, before reaching origin |
| Geolocation routing | Edge has IP-to-region built-in |
| Quick API responses (< 50 ms work) | Cold starts are sub-100ms |
| Streaming responses | First chunk emitted very fast |
| A/B test variants | Decision happens close to the user |
| Use case | Why Edge fails |
|---|---|
| Database with TCP driver | Edge runtimes don't support raw TCP |
| Long-running work (> 1s) | Edge has tight wall-clock and memory limits |
| Native Node modules (sharp, bcrypt) | Edge can't run native bindings |
| Big libraries (Stripe SDK) | Edge bundle size limits (1–4 MB) |
| File system access | Edge has no fs |
Use HTTP-only database drivers:
@neondatabase/serverless over HTTP@planetscale/database over HTTPThese connect over HTTPS, not raw TCP, so they work in Edge.
1// lib/db.ts
2import { neon } from "@neondatabase/serverless";
3
4export const sql = neon(process.env.DATABASE_URL!);1// app/api/users/route.ts
2export const runtime = "edge";
3import { sql } from "@/lib/db";
4
5export async function GET() {
6 const users = await sql`SELECT id, name FROM users LIMIT 10`;
7 return Response.json(users);
8}middleware.ts is always edge-runtime. You cannot use fs, native modules, or heavy Node-only libraries in it.
1// middleware.ts
2import { NextResponse, type NextRequest } from "next/server";
3
4export function middleware(req: NextRequest) {
5 const country = req.geo?.country ?? "US";
6 if (country === "EU") {
7 return NextResponse.rewrite(new URL("/eu" + req.nextUrl.pathname, req.url));
8 }
9}
10
11export const config = {
12 matcher: ["/((?!_next/static|api/health).*)"],
13};1// app/api/feed/route.ts
2export const runtime = "edge";
3
4export async function GET() {
5 const stream = new ReadableStream({
6 async start(controller) {
7 for (let i = 0; i < 5; i++) {
8 controller.enqueue(`tick ${i}\n`);
9 await new Promise((r) => setTimeout(r, 200));
10 }
11 controller.close();
12 },
13 });
14 return new Response(stream, {
15 headers: { "Content-Type": "text/plain; charset=utf-8" },
16 });
17}The first byte goes out almost instantly. Great for token-by-token LLM streaming.