When to Use Edge vs Serverless Functions for API Calls
This guide is part of Vercel Edge Runtime vs Cloudflare Workers.
The edge vs serverless decision is not about which is “better”—it is about matching the runtime’s constraints to the API’s requirements. Getting this wrong shows up as 500 errors from missing Node.js modules, 413 errors from oversized payloads, or cold-start spikes that degrade perceived performance. Getting it right is largely a matter of following a decision tree based on your API’s actual characteristics, and the constraint that forces the split is the edge V8 isolate’s Web-API-only surface.
Symptom: Routing Misalignment
These symptoms indicate a mismatch between runtime and workload:
- Cold-start spikes during low traffic: P99 response times jumping from 50 ms to 800+ ms during lulls. This typically means a serverless function is deployed to an edge route with inadequate warm-up, or an edge function is doing heavy initialization work that belongs in a serverless container.
- 413 or immediate 500 errors: Uploading multipart form data or large JSON fails at the edge because the payload exceeds the 1 MB limit. A 500 with
require('fs') not foundmeans Node.js built-ins are being called in an edge runtime. - Missing authentication headers:
Set-Cookiebeing stripped or custom auth headers not propagating is a sign the edge runtime is dropping non-standard headers without the explicit forwarding configuration required.
Core Constraint Comparison
| Constraint | Edge Runtime | Serverless Function |
|---|---|---|
| Max payload | ~1 MB request/response | 4.5 MB+ (platform-dependent) |
| Execution timeout | 30 s (Cloudflare) / 1000 ms (Vercel) | 15 min (Lambda) / 300 s (Vercel) |
| Runtime | V8 Isolate: Web APIs only | Full Node.js / Python / Go with OS access |
| Cold start | Near-zero (Cloudflare) / < 100 ms (Vercel) | 100 ms – 2 s depending on runtime and traffic |
| Cache integration | CDN-native via s-maxage, stale-while-revalidate |
Requires explicit Cache-Control and CDN configuration |
| Database connections | HTTP-only (Hyperdrive, Prisma Accelerate) | Direct TCP connections; connection pooling supported |
| Memory | 128 MB (Cloudflare, Vercel) | 128 MB – 10 GB (Lambda) |
Decision Criteria
Use edge when ALL of the following are true:
- The API uses only Web APIs (
fetch,WebCrypto,ReadableStream). - Payloads are below 1 MB.
- Sub-100 ms TTFB is required globally.
- The operation is stateless (JWT validation, geo-routing, A/B testing, header injection).
Use serverless when ANY of the following are true:
- The code uses Node.js built-ins (
fs,child_process, native TCP connections). - Payloads routinely exceed 1 MB (file uploads, large JSON exports).
- The operation requires a long-running connection or background processing > 30 s.
- The logic uses database drivers that open direct TCP connections (Prisma without Accelerate, raw
pg,mysql2).
Step 1: Audit Dependency Compatibility
Before routing to edge, verify your API’s imports:
// Edge-incompatible
import { createHash } from 'crypto'; // Node.js module
import fs from 'fs/promises'; // No filesystem at edge
import { Pool } from 'pg'; // Raw TCP; not available
// Edge-compatible equivalents
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data));
// Use platform KV / Blob storage instead of fs
// Use Prisma Accelerate, Hyperdrive, or an HTTP API instead of pg
Step 2: Route Traffic by Payload and Route Pattern
In Next.js, use middleware to direct traffic based on request characteristics:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
const contentLength = Number(req.headers.get('content-length') ?? '0');
// Large payloads must go to serverless; edge runtime will reject them
if (req.nextUrl.pathname.startsWith('/api/upload') || contentLength > 900_000) {
// Rewrite to a serverless-backed route
const url = req.nextUrl.clone();
url.pathname = `/api/serverless${url.pathname.replace('/api', '')}`;
return NextResponse.rewrite(url);
}
// Cache GET responses at the edge CDN layer for public endpoints
if (req.method === 'GET' && req.nextUrl.pathname.startsWith('/api/public/')) {
const res = NextResponse.next();
res.headers.set('Cache-Control', 's-maxage=60, stale-while-revalidate=300');
return res;
}
return NextResponse.next();
}
export const config = { matcher: ['/api/:path*'] };
Step 3: Apply Runtime-Specific Caching and Timeout Rules
Edge routes:
- Use
s-maxagefor shared (CDN) caching;max-agefor browser caching. - Headers are effectively immutable after the response is created; clone to modify.
Cache-Control: no-storeon authenticated routes prevents cache poisoning.
Serverless routes:
- Implement connection pooling for database clients to amortize cold-start connection overhead.
- Guard against runaway executions with an explicit timeout:
const runWithTimeout = async <T>(
promise: Promise<T>,
limitMs: number,
label: string
): Promise<T> => {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Timeout: ${label} exceeded ${limitMs}ms`)), limitMs)
);
return Promise.race([promise, timeout]);
};
// For a 300 s platform limit, guard at 290 s to leave headroom for response serialization
const result = await runWithTimeout(heavyDatabaseQuery(), 290_000, 'heavyDatabaseQuery');
Production Validation Checklist
- Simulate edge payload limits: Inject a 1.1 MB JSON payload to confirm your edge route returns
413and your serverless fallback handles it correctly. - Verify
Cache-Controlin production:curl -I https://your-domain.com/api/public/endpoint | grep cache-control. Missings-maxagemeans the CDN is not caching the response and you are invoking the function on every request. - Check cold-start duration: Use
wrangler tail(Cloudflare) orvercel logsto confirminitDurationfor serverless functions. If consistently > 500 ms, apply the dependency optimization patterns from Managing Cold Starts in Serverless Environments. - Monitor memory: Edge isolates cap at 128 MB. Watch OOM errors in platform dashboards; they indicate the workload belongs in a serverless function.
Pre-Ship Checklist
- Edge routes import zero
node: - A 1.1 MB payload returns
413 - Public GET responses carry
s-maxageandstale-while-revalidate, confirmed viacurl -I
Frequently Asked Questions
When should an API call run at the edge instead of in a serverless function?
Run it at the edge when it uses only Web APIs (fetch, crypto.subtle, ReadableStream), handles payloads under 1 MB, is stateless, and benefits from sub-100 ms global latency — JWT validation, geo-routing, A/B test assignment, and header injection are ideal. Anything needing Node built-ins, payloads over 1 MB, or long-lived TCP connections belongs in a serverless function.
Why do I get a 413 error from my edge API route?
Edge runtimes cap request and response bodies near 1 MB, so a multipart upload or large JSON export exceeds the limit and is rejected with 413. Detect oversized requests in middleware via the content-length header and rewrite them to a serverless-backed route that can accept the larger payload.
Can I open a direct database connection from an edge function?
No — edge isolates cannot open raw TCP sockets, so drivers like pg and mysql2 fail. Use an HTTP-based data path such as Cloudflare Hyperdrive, Prisma Accelerate, or a database HTTP API, or move the query into a serverless function that connects over TCP with connection pooling.
How do I route some requests to the edge and others to serverless in Next.js?
Use middleware.ts to inspect request characteristics — path prefix, content-length, content type — and call NextResponse.rewrite() to redirect heavy requests to a serverless-backed route while leaving light requests at the edge. The middleware itself stays edge-only with no Node dependencies.
Conclusion
The decision is mechanical: if the API requires Node.js built-ins, payloads > 1 MB, or long-running TCP connections, it belongs in a serverless function. If it only needs Web APIs, handles small payloads, and benefits from sub-100 ms global latency, it belongs at the edge. Middleware that routes traffic between these two tiers based on request characteristics—payload size, path prefix, content type—is itself a valid edge-only workload with no Node.js dependencies and minimal compute requirements.