Edge vs Serverless Functions in Next.js - When to Use Each
Confused about edge vs serverless? Learn when to use each runtime in Next.js with real performance data and practical examples.
Last month I spent two hours debugging why an API route was taking 800ms on first request. The endpoint itself was fast - just fetching user data from a database. Turns out, the cold start was eating 700ms before my code even ran.
Switching to the edge runtime dropped that to under 100ms. But then I hit a wall: my database driver didn't work on edge.
This is the edge vs serverless tradeoff in a nutshell. Both have their place. The trick is knowing which to use when.
What Are We Actually Comparing?
Serverless Functions (Node.js runtime) are the default in Next.js. They run in a container that spins up on demand, executes your code, then potentially shuts down. You get full Node.js - every npm package, native modules, the works.
Edge Functions run on Vercel's edge network - the same infrastructure that serves your static files. They execute closer to users (in 30+ regions vs 1-2 for serverless), start almost instantly, but run in a limited V8 environment without Node.js APIs.
Think of it this way:
- Serverless = full kitchen, but it takes time to turn on the stove
- Edge = microwave, instant but can't make everything
The Performance Difference
Cold starts are where edge destroys serverless:
| Metric | Serverless | Edge |
|---|---|---|
| Cold start | 250-500ms | ~0ms |
| Warm execution | ~50ms | ~25ms |
| Memory limit | 1024MB default | 128MB |
| Timeout | 10s (hobby) / 60s (pro) | 25s |
That 250-500ms cold start on serverless isn't a bug - it's the time needed to spin up a container. Edge functions skip this entirely because they run on V8 isolates that start in milliseconds.
For user-facing requests, this matters. A lot.
When to Use Edge Functions
Edge is perfect when you need speed and don't need Node.js features:
Authentication Checks
Validating a JWT or session cookie before letting a request through? Edge.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export const config = {
matcher: '/dashboard/:path*',
}
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Verify JWT (using edge-compatible library)
// ...
return NextResponse.next()
}Middleware in Next.js runs on edge by default. It executes before the request hits your page or API route.
A/B Testing and Feature Flags
Deciding which variant to show based on a cookie or header? Edge.
// middleware.ts
export function middleware(request: NextRequest) {
const bucket = request.cookies.get('ab-bucket')?.value
if (!bucket) {
// Assign to bucket
const newBucket = Math.random() < 0.5 ? 'control' : 'variant'
const response = NextResponse.next()
response.cookies.set('ab-bucket', newBucket)
return response
}
if (bucket === 'variant' && request.nextUrl.pathname === '/pricing') {
return NextResponse.rewrite(new URL('/pricing-v2', request.url))
}
return NextResponse.next()
}Geolocation-Based Routing
Redirecting users to region-specific content? Edge has geolocation data built in.
// middleware.ts
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US'
if (country === 'DE' && !request.nextUrl.pathname.startsWith('/de')) {
return NextResponse.redirect(new URL('/de' + request.nextUrl.pathname, request.url))
}
return NextResponse.next()
}Simple API Responses
Returning data that doesn't need a database or complex processing:
// app/api/health/route.ts
export const runtime = 'edge'
export function GET() {
return Response.json({ status: 'ok', timestamp: Date.now() })
}When to Use Serverless (Node.js)
Serverless is the right choice when you need the full Node.js ecosystem:
Database Connections
Most database drivers require Node.js. Prisma, Drizzle with node-postgres, mysql2 - they won't work on edge.
// app/api/users/route.ts
// No runtime export = defaults to Node.js
import { db } from '@/lib/db'
export async function GET() {
const users = await db.query.users.findMany()
return Response.json(users)
}Some databases have edge-compatible clients (Planetscale, Neon with their HTTP drivers, Turso), but if you're using standard Postgres or MySQL, stick with serverless.
Heavy Processing
CPU-intensive tasks like image manipulation, PDF generation, or data transformation belong in serverless where you have more memory and longer timeouts.
// app/api/generate-report/route.ts
import { generatePDF } from '@/lib/pdf'
export async function POST(request: Request) {
const data = await request.json()
// This might take several seconds and need lots of memory
const pdf = await generatePDF(data)
return new Response(pdf, {
headers: { 'Content-Type': 'application/pdf' }
})
}npm Packages with Native Dependencies
Anything that uses native code (sharp for images, bcrypt for hashing, etc.) needs Node.js:
// app/api/upload/route.ts
import sharp from 'sharp'
export async function POST(request: Request) {
const formData = await request.formData()
const file = formData.get('image') as File
const buffer = await file.arrayBuffer()
const optimized = await sharp(Buffer.from(buffer))
.resize(800, 600)
.webp({ quality: 80 })
.toBuffer()
// Upload to storage...
}Streaming Responses
While edge supports streaming, complex streaming scenarios with backpressure handling work better in Node.js:
// app/api/export/route.ts
import { db } from '@/lib/db'
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
const cursor = db.query.largeTable.findMany({
cursor: { take: 1000 }
})
for await (const batch of cursor) {
controller.enqueue(JSON.stringify(batch) + '\n')
}
controller.close()
}
})
return new Response(stream)
}How to Configure Runtime in Next.js
Setting the runtime is a one-liner:
// For edge
export const runtime = 'edge'
// For Node.js (explicit, though it's the default)
export const runtime = 'nodejs'This works in:
- Route handlers (
app/api/*/route.ts) - Page components (
app/*/page.tsx) - Layout components (
app/*/layout.tsx)
Middleware always runs on edge - you can't change it.
Mixing Both in the Same App
This is where it gets interesting. You can (and should) use both runtimes in the same app:
app/
├── api/
│ ├── auth/
│ │ └── route.ts # Edge - fast token validation
│ ├── users/
│ │ └── route.ts # Node.js - database queries
│ └── health/
│ └── route.ts # Edge - simple response
├── middleware.ts # Edge - always
└── dashboard/
└── page.tsx # Node.js - server component with DBThe decision tree is simple:
- Does it need Node.js APIs or npm packages with native deps? → Serverless
- Does it connect to a traditional database? → Serverless (usually)
- Is it middleware or auth checking? → Edge
- Is latency critical and the logic simple? → Edge
- Not sure? → Serverless (safer default)
Edge-Compatible Database Options
If you want edge performance with database access, these work:
- Turso (libSQL) - SQLite at the edge
- Planetscale - MySQL with HTTP driver
- Neon - Postgres with HTTP driver
- Cloudflare D1 - SQLite (if on Cloudflare)
- Upstash Redis - For caching and simple data
These use HTTP-based protocols instead of persistent connections, making them edge-compatible.
The Gotchas
A few things that will bite you:
1. No fs on edge - Can't read files from disk. Everything needs to be in your bundle or fetched over HTTP.
2. No process.env differences - Environment variables work the same, but some Node.js-specific env vars won't exist.
3. Limited crypto - Web Crypto API is available, but Node.js crypto module isn't. Libraries like jose work; jsonwebtoken doesn't.
4. Size limits - Edge function bundles are limited to 1MB (after gzip). Heavy dependencies push you over fast.
5. Cold starts still exist on edge - They're just much faster (~5ms vs ~500ms). First request to a region might be slightly slower.
What We Use in vibestacks
In vibestacks, we default to serverless for most API routes because we use Drizzle with standard Postgres. The cold starts are acceptable for authenticated API calls.
But middleware runs on edge for auth checks - validating sessions before requests hit the server. This keeps the perceived latency low for logged-in users navigating the dashboard.
For health checks and simple webhooks, we use edge. No reason to spin up a full Node.js container just to return { status: 'ok' }.
The Bottom Line
Don't overthink it:
- Edge = fast, limited, great for middleware and simple responses
- Serverless = flexible, full Node.js, slightly slower cold starts
Start with serverless (the default), then move specific routes to edge when you need the speed and can work within the constraints.
The performance difference is real. But shipping a working app beats optimizing one that doesn't exist yet.
Questions about edge vs serverless? Hit me up at raman@vibestacks.io or on LinkedIn.
Read more
Background Jobs in Next.js - Solving the Serverless Problem
Need cron jobs in Next.js? Here's how to run scheduled tasks, background processing, and async workflows on serverless platforms.
Rate Limiting Your Next.js API Routes - The Complete Guide
Protect your API from abuse with rate limiting. From simple in-memory solutions to production-ready Redis implementations.

Why Your Next.js App Feels Broken (And How Tests Save Your Refactor)
A practical look at Next.js caching strategies and why high level tests are your safety net when paying down tech debt