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.

Edge vs serverless routing decision An incoming request is checked for Node built-in usage, payload size, and connection duration; matches route to a serverless function, otherwise to the edge. API request Node built-ins? payload > 1 MB? long TCP? Serverless function full Node, larger limits Edge function Web APIs, sub-100 ms TTFB yes no
Route a request to serverless when it needs Node built-ins, large payloads, or long-lived connections; otherwise keep it at the edge.

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 found means Node.js built-ins are being called in an edge runtime.
  • Missing authentication headers: Set-Cookie being 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:

  1. The API uses only Web APIs (fetch, WebCrypto, ReadableStream).
  2. Payloads are below 1 MB.
  3. Sub-100 ms TTFB is required globally.
  4. The operation is stateless (JWT validation, geo-routing, A/B testing, header injection).

Use serverless when ANY of the following are true:

  1. The code uses Node.js built-ins (fs, child_process, native TCP connections).
  2. Payloads routinely exceed 1 MB (file uploads, large JSON exports).
  3. The operation requires a long-running connection or background processing > 30 s.
  4. 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-maxage for shared (CDN) caching; max-age for browser caching.
  • Headers are effectively immutable after the response is created; clone to modify.
  • Cache-Control: no-store on 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

  1. Simulate edge payload limits: Inject a 1.1 MB JSON payload to confirm your edge route returns 413 and your serverless fallback handles it correctly.
  2. Verify Cache-Control in production: curl -I https://your-domain.com/api/public/endpoint | grep cache-control. Missing s-maxage means the CDN is not caching the response and you are invoking the function on every request.
  3. Check cold-start duration: Use wrangler tail (Cloudflare) or vercel logs to confirm initDuration for serverless functions. If consistently > 500 ms, apply the dependency optimization patterns from Managing Cold Starts in Serverless Environments.
  4. 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-maxage and stale-while-revalidate, confirmed via curl -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.