Edge Computing & Edge Functions: The Future of Low-Latency Applications
Master edge computing architecture: Vercel Edge Functions, Cloudflare Workers, edge caching strategies, and understand the critical trade-offs between edge computing and traditional serverless. Learn how to build ultra-fast, globally distributed applications that respond in milliseconds.
1. Edge Computing Fundamentals
What is Edge Computing? Edge computing brings computation and data storage closer to the location where it's needed, reducing latency and bandwidth usage. Instead of processing requests in a central data center, edge functions run on distributed edge locations worldwide.
Why Edge Computing Matters: In a world where every millisecond counts, edge computing can reduce response times from 200-500ms (traditional serverless) to 10-50ms (edge functions). This is critical for real-time applications, global user bases, and performance-sensitive workloads.
Key Concepts:
โข Edge Locations: Distributed points of presence (PoPs) closer to end users
โข Cold Start Elimination: Edge functions are always warm, eliminating cold start latency
โข Global Distribution: Code runs in 200+ locations worldwide simultaneously
โข Request Routing: Automatic routing to the nearest edge location
โข Stateless Execution: Edge functions are stateless, enabling horizontal scaling
// โ BAD: Traditional Serverless (Centralized)// Request travels: User โ CDN โ Origin โ Lambda (us-east-1) โ Response// Total latency: ~300-500ms for users far from us-east-1export async function handler(event: APIGatewayEvent) {// Runs in single region (e.g., us-east-1)// User in Tokyo: 300ms+ latency// User in London: 200ms+ latency// User in Sydney: 400ms+ latencyconst data = await fetch('https://api.example.com/data');return {statusCode: 200,body: JSON.stringify(data)};}
// โ GOOD: Edge Function (Distributed)// Request travels: User โ Nearest Edge Location โ Response// Total latency: ~10-50ms globally// Vercel Edge Functionexport const config = {runtime: 'edge', // Runs on edge network};export default async function handler(request: Request) {// Runs in 200+ locations simultaneously// User in Tokyo: ~15ms latency// User in London: ~12ms latency// User in Sydney: ~18ms latencyconst data = await fetch('https://api.example.com/data');return new Response(JSON.stringify(data), {headers: { 'Content-Type': 'application/json' }});}
When to Use Edge Computing:
โข Real-time applications (chat, gaming, live updates)
โข Global user bases requiring low latency
โข A/B testing and personalization
โข Authentication and authorization
โข API route handlers
โข Image optimization and transformations
โข Bot detection and rate limiting
2. Vercel Edge Functions
Vercel Edge Functions run on V8 isolates at the edge, providing sub-50ms response times globally. They're built on the Web API standard (Request/Response), making them familiar to web developers while offering incredible performance.
Vercel Edge Functions Features:
โข Web API Standard: Uses Request/Response APIs (not Node.js)
โข V8 Isolates: Lightweight, fast startup (no cold starts)
โข Global Distribution: Runs in 200+ edge locations
โข TypeScript Support: Full TypeScript support out of the box
โข Streaming: Support for streaming responses
โข Middleware: Edge Middleware for request interception
// โ Vercel Edge Function - Basic Example// File: app/api/hello/route.tsexport const runtime = 'edge';export async function GET(request: Request) {const { searchParams } = new URL(request.url);const name = searchParams.get('name') || 'World';return new Response(JSON.stringify({message: `Hello, ${name}!`,timestamp: new Date().toISOString(),region: request.headers.get('x-vercel-id') || 'unknown'}),{status: 200,headers: {'Content-Type': 'application/json','Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300'}});}// โ Edge Function with Authenticationexport const runtime = 'edge';export async function GET(request: Request) {const authHeader = request.headers.get('authorization');if (!authHeader || !authHeader.startsWith('Bearer ')) {return new Response(JSON.stringify({ error: 'Unauthorized' }),{ status: 401 });}const token = authHeader.substring(7);// Verify token (can use Web Crypto API)const isValid = await verifyToken(token);if (!isValid) {return new Response(JSON.stringify({ error: 'Invalid token' }),{ status: 401 });}return new Response(JSON.stringify({ message: 'Authenticated successfully' }),{ status: 200 });}// โ Edge Function with External API Callexport const runtime = 'edge';export async function GET(request: Request) {try {// Edge functions can make fetch callsconst response = await fetch('https://api.example.com/data', {headers: {'Authorization': `Bearer ${process.env.API_KEY}`}});if (!response.ok) {throw new Error('API request failed');}const data = await response.json();// Transform data at the edgeconst transformed = {...data,processedAt: new Date().toISOString(),edgeLocation: request.headers.get('x-vercel-id')};return new Response(JSON.stringify(transformed), {headers: { 'Content-Type': 'application/json' }});} catch (error) {return new Response(JSON.stringify({ error: 'Failed to fetch data' }),{ status: 500 });}}// โ Edge Function with Streaming Responseexport const runtime = 'edge';export async function GET() {const encoder = new TextEncoder();const stream = new ReadableStream({async start(controller) {for (let i = 0; i < 10; i++) {const data = encoder.encode(`Chunk ${i}\n`);controller.enqueue(data);await new Promise(resolve => setTimeout(resolve, 100));}controller.close();}});return new Response(stream, {headers: {'Content-Type': 'text/plain','Transfer-Encoding': 'chunked'}});}// โ Edge Function with CORSexport const runtime = 'edge';export async function OPTIONS() {return new Response(null, {status: 204,headers: {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Methods': 'GET, POST, OPTIONS','Access-Control-Allow-Headers': 'Content-Type, Authorization','Access-Control-Max-Age': '86400'}});}export async function POST(request: Request) {const data = await request.json();return new Response(JSON.stringify({ success: true, data }), {status: 200,headers: {'Content-Type': 'application/json','Access-Control-Allow-Origin': '*'}});}
Vercel Edge Middleware
Edge Middleware runs before a request is processed, allowing you to intercept, modify, or redirect requests at the edge. This is perfect for authentication, A/B testing, geolocation-based routing, and more.
// โ Vercel Edge Middleware// File: middleware.tsimport { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';export function middleware(request: NextRequest) {// Get user's locationconst country = request.geo?.country || 'US';const city = request.geo?.city || 'Unknown';// A/B Testingconst cookie = request.cookies.get('ab-test');const variant = cookie?.value || (Math.random() > 0.5 ? 'A' : 'B');// Create responseconst response = NextResponse.next();// Set A/B test cookieresponse.cookies.set('ab-test', variant, {maxAge: 60 * 60 * 24 * 30 // 30 days});// Add custom headersresponse.headers.set('x-user-country', country);response.headers.set('x-user-city', city);response.headers.set('x-ab-variant', variant);// Redirect based on countryif (country === 'CN' && !request.nextUrl.pathname.startsWith('/cn')) {return NextResponse.redirect(new URL('/cn' + request.nextUrl.pathname, request.url));}return response;}// Configure which routes to run middleware onexport const config = {matcher: [/** Match all request paths except for the ones starting with:* - api (API routes)* - _next/static (static files)* - _next/image (image optimization files)* - favicon.ico (favicon file)*/'/((?!api|_next/static|_next/image|favicon.ico).*)',],};// โ Authentication Middlewareexport function middleware(request: NextRequest) {const token = request.cookies.get('auth-token');const isAuthPage = request.nextUrl.pathname.startsWith('/login');// Redirect to login if not authenticated (except auth pages)if (!token && !isAuthPage) {return NextResponse.redirect(new URL('/login', request.url));}// Redirect to home if authenticated and on login pageif (token && isAuthPage) {return NextResponse.redirect(new URL('/', request.url));}return NextResponse.next();}// โ Rate Limiting Middlewareconst rateLimitMap = new Map<string, { count: number; resetTime: number }>();export function middleware(request: NextRequest) {const ip = request.ip || 'unknown';const now = Date.now();const limit = 100; // requestsconst window = 60000; // 1 minuteconst record = rateLimitMap.get(ip);if (record) {if (now > record.resetTime) {// Reset windowrateLimitMap.set(ip, { count: 1, resetTime: now + window });} else {// Increment countrecord.count++;if (record.count > limit) {return new NextResponse('Rate limit exceeded', { status: 429 });}rateLimitMap.set(ip, record);}} else {rateLimitMap.set(ip, { count: 1, resetTime: now + window });}return NextResponse.next();}// โ Geolocation-Based Contentexport function middleware(request: NextRequest) {const country = request.geo?.country;const response = NextResponse.next();// Set country-specific headersresponse.headers.set('x-user-country', country || 'unknown');// Redirect to country-specific domainif (country === 'GB') {response.headers.set('x-content-region', 'uk');} else if (country === 'US') {response.headers.set('x-content-region', 'us');}return response;}
Vercel Edge Functions Limitations:
โข No Node.js APIs (use Web APIs instead)
โข 50MB memory limit
โข 30-second execution timeout
โข No file system access
โข Limited to Web Crypto API for cryptography
โข Cannot use native Node.js modules
3. Cloudflare Workers
Cloudflare Workers is a serverless platform that runs JavaScript, WebAssembly, or Rust code on Cloudflare's edge network. With 300+ data centers worldwide, Workers provide exceptional global performance and powerful edge computing capabilities.
Cloudflare Workers Features:
โข V8 Isolates: Fast, secure JavaScript execution
โข WebAssembly Support: Run Rust, C, C++ code at the edge
โข Durable Objects: Strongly consistent, globally distributed state
โข KV Storage: Global key-value store at the edge
โข R2 Storage: S3-compatible object storage
โข D1 Database: SQLite-compatible edge database
โข Workers AI: Run AI models at the edge
// โ Basic Cloudflare Worker// File: worker.jsexport default {async fetch(request, env, ctx) {const url = new URL(request.url);// Handle different routesif (url.pathname === '/api/hello') {return new Response(JSON.stringify({message: 'Hello from Cloudflare Workers!',timestamp: new Date().toISOString(),cf: {country: request.cf?.country,city: request.cf?.city,colo: request.cf?.colo // Cloudflare data center code}}), {headers: { 'Content-Type': 'application/json' }});}// Default responsereturn new Response('Not Found', { status: 404 });}};// โ Worker with Environment Variablesexport default {async fetch(request, env) {// Access environment variablesconst apiKey = env.API_KEY;const dbUrl = env.DATABASE_URL;// Make authenticated requestconst response = await fetch('https://api.example.com/data', {headers: {'Authorization': `Bearer ${apiKey}`}});const data = await response.json();return new Response(JSON.stringify(data), {headers: { 'Content-Type': 'application/json' }});}};// โ Worker with KV Storageexport default {async fetch(request, env) {const url = new URL(request.url);const key = url.searchParams.get('key');if (!key) {return new Response('Missing key parameter', { status: 400 });}// Read from KVconst value = await env.MY_KV_NAMESPACE.get(key);if (!value) {return new Response('Key not found', { status: 404 });}// Write to KVawait env.MY_KV_NAMESPACE.put(key, JSON.stringify({value,updatedAt: new Date().toISOString()}));return new Response(JSON.stringify({ key, value }), {headers: { 'Content-Type': 'application/json' }});}};// โ Worker with Durable Objects (Strongly Consistent State)// File: worker.jsexport default {async fetch(request, env) {const url = new URL(request.url);const id = env.COUNTER.idFromName('global-counter');const obj = env.COUNTER.get(id);// Forward request to Durable Objectreturn obj.fetch(request);}};// File: durable-object.jsexport class Counter {constructor(state, env) {this.state = state;this.env = env;}async fetch(request) {// Get current countlet count = await this.state.storage.get('count') || 0;const url = new URL(request.url);if (url.pathname === '/increment') {count++;await this.state.storage.put('count', count);}return new Response(JSON.stringify({ count }), {headers: { 'Content-Type': 'application/json' }});}}// โ Worker with R2 Storage (S3-compatible)export default {async fetch(request, env) {const url = new URL(request.url);if (request.method === 'PUT' && url.pathname.startsWith('/upload/')) {const key = url.pathname.replace('/upload/', '');const body = await request.arrayBuffer();// Upload to R2await env.MY_BUCKET.put(key, body, {httpMetadata: {contentType: request.headers.get('Content-Type')}});return new Response(JSON.stringify({ success: true, key }), {headers: { 'Content-Type': 'application/json' }});}if (request.method === 'GET' && url.pathname.startsWith('/download/')) {const key = url.pathname.replace('/download/', '');const object = await env.MY_BUCKET.get(key);if (!object) {return new Response('Not found', { status: 404 });}return new Response(object.body, {headers: {'Content-Type': object.httpMetadata.contentType}});}return new Response('Method not allowed', { status: 405 });}};// โ Worker with D1 Database (SQLite at the edge)export default {async fetch(request, env) {const url = new URL(request.url);if (url.pathname === '/users' && request.method === 'GET') {// Query D1 databaseconst { results } = await env.DB.prepare('SELECT * FROM users LIMIT 10').all();return new Response(JSON.stringify(results), {headers: { 'Content-Type': 'application/json' }});}if (url.pathname === '/users' && request.method === 'POST') {const data = await request.json();// Insert into D1const result = await env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind(data.name, data.email).run();return new Response(JSON.stringify({ id: result.meta.last_row_id }), {headers: { 'Content-Type': 'application/json' }});}return new Response('Not found', { status: 404 });}};// โ Worker with Workers AI (Run AI models at the edge)export default {async fetch(request, env) {const url = new URL(request.url);if (url.pathname === '/translate' && request.method === 'POST') {const { text, targetLang } = await request.json();// Use Workers AI for translationconst response = await env.AI.run('@cf/meta/m2m100-1.2b', {text,target_lang: targetLang,source_lang: 'en'});return new Response(JSON.stringify({ translation: response.translated_text }), {headers: { 'Content-Type': 'application/json' }});}if (url.pathname === '/summarize' && request.method === 'POST') {const { text } = await request.json();// Summarize text using AIconst response = await env.AI.run('@cf/facebook/bart-large-cnn', {input_text: text,max_length: 100});return new Response(JSON.stringify({ summary: response.summary }), {headers: { 'Content-Type': 'application/json' }});}return new Response('Not found', { status: 404 });}};// โ Worker with WebAssembly (Rust)// First, compile Rust to WASM: wasm-pack build --target web// Then use in Worker:import wasmModule from './pkg/rust_module.js';export default {async fetch(request) {// Initialize WASM moduleawait wasmModule.default();// Call WASM functionconst result = wasmModule.compute_heavy_operation(1000000);return new Response(JSON.stringify({ result }), {headers: { 'Content-Type': 'application/json' }});}};// โ Worker with Cachingexport default {async fetch(request, env, ctx) {const cacheKey = new Request(request.url, request);const cache = caches.default;// Try to get from cachelet response = await cache.match(cacheKey);if (!response) {// Fetch fresh dataresponse = await fetch('https://api.example.com/data');// Clone response for cachingresponse = new Response(response.body, response);response.headers.set('Cache-Control', 'public, max-age=3600');// Store in cachectx.waitUntil(cache.put(cacheKey, response.clone()));}return response;}};// โ Worker with Request Transformationexport default {async fetch(request) {// Modify request before forwardingconst modifiedRequest = new Request(request.url, {method: request.method,headers: {...Object.fromEntries(request.headers),'X-Custom-Header': 'edge-processed','X-User-Country': request.cf?.country || 'unknown'},body: request.body});// Forward to originconst response = await fetch(modifiedRequest);// Modify responseconst modifiedResponse = new Response(response.body, {status: response.status,headers: {...Object.fromEntries(response.headers),'X-Processed-By': 'cloudflare-workers'}});return modifiedResponse;}};
Cloudflare Workers vs Vercel Edge Functions
# โ Choosing Wrong Platform// Using Vercel Edge Functions for:- Complex state management (need Durable Objects)- Large file processing (need R2 storage)- SQL database operations (need D1)- AI/ML inference (need Workers AI)- WebAssembly workloads// Using Cloudflare Workers for:- Next.js-specific features- React Server Components- Vercel-specific integrations
# โ Choosing Right Platform// Use Vercel Edge Functions when:- Building Next.js applications- Need seamless Next.js integration- Want simple Request/Response API- Focus on API routes and middleware- Using Vercel deployment platform// Use Cloudflare Workers when:- Need persistent storage (KV, R2, D1)- Require strongly consistent state (Durable Objects)- Want AI/ML at the edge (Workers AI)- Need WebAssembly support- Building standalone edge services- Require advanced caching strategies
Cloudflare Workers Advantages:
โข More edge locations (300+ vs 200+)
โข Persistent storage options (KV, R2, D1)
โข Durable Objects for stateful applications
โข Workers AI for edge ML inference
โข WebAssembly support
โข Longer execution time limits (up to 30 seconds)
โข More generous free tier
4. Edge Caching Strategies
Effective edge caching is crucial for performance. Learn how to implement cache invalidation, stale-while-revalidate patterns, edge-side includes, and cache warming strategies that maximize hit rates while ensuring data freshness.
Caching Strategies:
โข Cache-Control Headers: Control cache behavior with HTTP headers
โข Stale-While-Revalidate: Serve stale content while fetching fresh data
โข Cache Tags: Invalidate related content together
โข Edge-Side Includes: Compose pages from cached fragments
โข Cache Warming: Pre-populate cache before traffic spikes
โข Vary Headers: Cache different versions based on request headers
// โ Cache-Control Headers Strategyexport const runtime = 'edge';export async function GET(request: Request) {const data = await fetchData();return new Response(JSON.stringify(data), {headers: {'Content-Type': 'application/json',// Cache for 1 hour, allow stale for 1 day'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',// Or: Cache for 5 minutes, no stale// 'Cache-Control': 'public, max-age=300, must-revalidate',// Or: Never cache// 'Cache-Control': 'no-store, no-cache, must-revalidate'}});}// โ Stale-While-Revalidate Patternexport const runtime = 'edge';const CACHE_DURATION = 3600; // 1 hourconst STALE_DURATION = 86400; // 1 dayexport async function GET(request: Request) {const cacheKey = request.url;const cache = caches.default;// Try to get from cacheconst cachedResponse = await cache.match(cacheKey);if (cachedResponse) {const cacheDate = cachedResponse.headers.get('date');const age = (Date.now() - new Date(cacheDate).getTime()) / 1000;// If within cache duration, return immediatelyif (age < CACHE_DURATION) {return cachedResponse;}// If within stale duration, return stale but revalidateif (age < STALE_DURATION) {// Revalidate in backgroundrevalidateCache(cacheKey, cache);// Return stale responsereturn cachedResponse;}}// Fetch fresh dataconst data = await fetchFreshData();const response = new Response(JSON.stringify(data), {headers: {'Content-Type': 'application/json','Cache-Control': `public, s-maxage=\${CACHE_DURATION}, stale-while-revalidate=\${STALE_DURATION}`,'Date': new Date().toUTCString()}});// Store in cacheawait cache.put(cacheKey, response.clone());return response;}async function revalidateCache(cacheKey: string, cache: Cache) {try {const data = await fetchFreshData();const response = new Response(JSON.stringify(data), {headers: {'Content-Type': 'application/json','Cache-Control': `public, s-maxage=\${CACHE_DURATION}, stale-while-revalidate=\${STALE_DURATION}`,'Date': new Date().toUTCString()}});await cache.put(cacheKey, response);} catch (error) {console.error('Cache revalidation failed:', error);}}// โ Cache Tags for Invalidationexport const runtime = 'edge';export async function GET(request: Request) {const { searchParams } = new URL(request.url);const userId = searchParams.get('userId');const data = await fetchUserData(userId);return new Response(JSON.stringify(data), {headers: {'Content-Type': 'application/json','Cache-Control': 'public, s-maxage=3600',// Tag cache entries for invalidation'Cache-Tag': `user:${userId},users:list`}});}// Invalidate cache by tag (webhook or API call)export async function POST(request: Request) {const { userId, action } = await request.json();if (action === 'invalidate') {// Invalidate all caches with this tagawait invalidateCacheTag(`user:${userId}`);}return new Response(JSON.stringify({ success: true }));}// โ Vary Headers for Different Cache Versionsexport const runtime = 'edge';export async function GET(request: Request) {const acceptLanguage = request.headers.get('Accept-Language') || 'en';const userAgent = request.headers.get('User-Agent') || '';const isMobile = /Mobile|Android|iPhone/i.test(userAgent);const data = await fetchData(acceptLanguage, isMobile);return new Response(JSON.stringify(data), {headers: {'Content-Type': 'application/json','Cache-Control': 'public, s-maxage=3600',// Cache different versions based on headers'Vary': 'Accept-Language, User-Agent'}});}// โ Edge-Side Includes (ESI) Pattern// Compose pages from cached fragmentsexport const runtime = 'edge';export async function GET(request: Request) {// Fetch multiple fragments in parallelconst [header, content, footer] = await Promise.all([fetchFragment('header'),fetchFragment('content'),fetchFragment('footer')]);// Compose page from fragmentsconst page = `<!DOCTYPE html><html><head><title>My Page</title></head><body>${await header.text()}${await content.text()}${await footer.text()}</body></html>`;return new Response(page, {headers: {'Content-Type': 'text/html','Cache-Control': 'public, s-maxage=3600'}});}async function fetchFragment(name: string) {const cache = caches.default;const cacheKey = `https://api.example.com/fragments/${name}`;let response = await cache.match(cacheKey);if (!response) {response = await fetch(cacheKey);response = new Response(response.body, {headers: {...Object.fromEntries(response.headers),'Cache-Control': 'public, s-maxage=7200'}});await cache.put(cacheKey, response.clone());}return response;}// โ Cache Warming Strategy// Pre-populate cache before traffic spikesexport async function warmCache() {const endpoints = ['/api/popular-products','/api/featured-articles','/api/homepage-data'];const cache = caches.default;for (const endpoint of endpoints) {try {const response = await fetch(`https://your-domain.com${endpoint}`);const cacheKey = `https://your-domain.com${endpoint}`;await cache.put(cacheKey, response.clone());console.log(`Warmed cache for ${endpoint}`);} catch (error) {console.error(`Failed to warm cache for ${endpoint}:`, error);}}}// Call warmCache() on schedule (cron job) or before expected traffic// โ Conditional Requests (ETag/Last-Modified)export const runtime = 'edge';export async function GET(request: Request) {const data = await fetchData();const etag = generateETag(data);const lastModified = new Date().toUTCString();// Check if client has cached versionconst ifNoneMatch = request.headers.get('If-None-Match');const ifModifiedSince = request.headers.get('If-Modified-Since');if (ifNoneMatch === etag) {// Client has current versionreturn new Response(null, { status: 304 }); // Not Modified}if (ifModifiedSince && new Date(ifModifiedSince) >= new Date(lastModified)) {return new Response(null, { status: 304 });}return new Response(JSON.stringify(data), {headers: {'Content-Type': 'application/json','ETag': etag,'Last-Modified': lastModified,'Cache-Control': 'public, max-age=3600'}});}function generateETag(data: any): string {// Simple ETag generation (use crypto for production)return `"\${Buffer.from(JSON.stringify(data)).toString('base64').slice(0, 27)}"`;}// โ Cache Partitioning by User Segmentexport const runtime = 'edge';export async function GET(request: Request) {const userSegment = getUserSegment(request);const cacheKey = `${request.url}:segment:${userSegment}`;const cache = caches.default;let response = await cache.match(cacheKey);if (!response) {const data = await fetchSegmentData(userSegment);response = new Response(JSON.stringify(data), {headers: {'Content-Type': 'application/json','Cache-Control': 'public, s-maxage=1800', // 30 minutes'Vary': 'Cookie' // Cache per user segment}});await cache.put(cacheKey, response.clone());}return response;}function getUserSegment(request: Request): string {// Determine user segment (e.g., free, premium, enterprise)const cookie = request.headers.get('Cookie');if (cookie?.includes('premium=true')) return 'premium';if (cookie?.includes('enterprise=true')) return 'enterprise';return 'free';}
Cache Strategy Decision Tree:
โข Static Content: Long TTL (24h+), no revalidation
โข Dynamic Content: Short TTL (5-60min), stale-while-revalidate
โข User-Specific: Private cache, vary by cookie/header
โข Real-Time Data: No cache or very short TTL (1-5min)
โข Heavy Computations: Long TTL, cache tags for invalidation
5. Edge vs Serverless: Critical Trade-offs
Understanding when to use edge functions versus traditional serverless is crucial for building optimal architectures. Each has distinct advantages, limitations, and use cases that senior engineers must understand.
# โ BAD: Using Edge Functions for Everything// Using edge functions for:- Heavy database operations (need connection pooling)- Long-running computations (30s timeout)- File system operations (not available)- Complex Node.js dependencies (not supported)- WebSocket connections (limited support)- Large memory requirements (>50MB)// Result:- Performance issues- Function timeouts- Missing features- Higher costs- Complex workarounds
# โ GOOD: Right Tool for Right Job// Use Edge Functions for:- Authentication/authorization- Request routing & middleware- A/B testing & personalization- API route handlers- Image optimization- Bot detection- Rate limiting- Geo-based routing// Use Traditional Serverless for:- Database-heavy operations- Long-running tasks- File processing- Complex Node.js apps- WebSocket servers- Background jobs- ML model training
Detailed Comparison
Latency & Performance
Edge Functions: 10-50ms globally, no cold starts, always warm
Serverless: 100-500ms (varies by region), cold starts can add 1-5 seconds
Execution Environment
Edge Functions: V8 isolates, Web APIs only, limited runtime APIs
Serverless: Full Node.js/Python/etc runtime, all standard libraries
Execution Time Limits
Edge Functions: 30 seconds (Vercel), 30 seconds (Cloudflare)
Serverless: Up to 15 minutes (AWS Lambda), configurable
Memory & Resources
Edge Functions: 50-128MB memory, no disk storage
Serverless: Up to 10GB memory (AWS Lambda), ephemeral disk storage
Database Connections
Edge Functions: HTTP-based databases only, no connection pooling
Serverless: Full database drivers, connection pooling possible
Cost Structure
Edge Functions: Pay per request, very low cost per invocation
Serverless: Pay per request + execution time, can be expensive for long-running tasks
State Management
Edge Functions: Stateless only, use external storage (KV, Redis)
Serverless: Can maintain state in memory (within execution), use databases
// โ Hybrid Architecture: Edge + Serverless// Edge Function: Fast authentication & routingexport const runtime = 'edge';export async function GET(request: Request) {// Fast auth check at edgeconst token = request.headers.get('Authorization');if (!token || !await verifyTokenAtEdge(token)) {return new Response('Unauthorized', { status: 401 });}// Route to appropriate backendconst user = await getUserFromToken(token);const backendUrl = user.plan === 'enterprise'? process.env.ENTERPRISE_API_URL: process.env.STANDARD_API_URL;// Proxy to serverless backend for heavy operationsconst response = await fetch(`${backendUrl}/api/data`, {headers: {'Authorization': token,'X-Edge-Processed': 'true'}});return response;}// Serverless Function: Heavy database operations// File: api/heavy-operation/route.ts (Next.js API route)export async function POST(request: Request) {// Full Node.js runtime availableconst { Pool } = require('pg');const pool = new Pool({connectionString: process.env.DATABASE_URL,max: 20, // Connection poolingidleTimeoutMillis: 30000});// Complex database queryconst result = await pool.query(`WITH complex_cte AS (SELECT * FROM large_tableWHERE created_at > NOW() - INTERVAL '30 days')SELECTcategory,COUNT(*) as count,AVG(price) as avg_priceFROM complex_cteGROUP BY categoryORDER BY count DESC`);// Process resultsconst processed = result.rows.map(row => ({...row,formatted_price: formatCurrency(row.avg_price)}));await pool.end();return Response.json(processed);}// โ Decision Matrix Implementationexport function shouldUseEdge(request: Request): boolean {const url = new URL(request.url);const path = url.pathname;// Use edge for:const edgePaths = ['/api/auth','/api/middleware','/api/rate-limit','/api/geo','/api/personalize'];if (edgePaths.some(p => path.startsWith(p))) {return true;}// Use serverless for:const serverlessPaths = ['/api/reports','/api/analytics','/api/export','/api/batch'];if (serverlessPaths.some(p => path.startsWith(p))) {return false;}// Default: use edge for speedreturn true;}// โ Cost Optimization: Edge for High Traffic, Serverless for Low Trafficexport async function routeRequest(request: Request) {const endpoint = new URL(request.url).pathname;const trafficLevel = await getTrafficLevel(endpoint);if (trafficLevel === 'high') {// High traffic: use edge (lower cost per request)return handleAtEdge(request);} else {// Low traffic: use serverless (better for infrequent requests)return handleAtServerless(request);}}// โ Performance Optimization: Edge for Simple, Serverless for Complexexport function chooseRuntime(operation: Operation): 'edge' | 'serverless' {const complexity = estimateComplexity(operation);if (complexity.simple && complexity.latencySensitive) {return 'edge'; // Fast, simple operations}if (complexity.needsDatabase || complexity.longRunning) {return 'serverless'; // Complex operations}return 'edge'; // Default to edge for speed}
Architecture Decision Framework:
Choose Edge When:
โข Latency is critical (<50ms required)
โข Operation is stateless
โข Simple request/response pattern
โข High request volume
โข Global user base
โข Authentication/authorization
Choose Serverless When:
โข Need full runtime capabilities
โข Long-running operations (>30s)
โข Database connection pooling required
โข Complex dependencies
โข File system access needed
โข Background job processing
Real-World Architecture Patterns
// โ Pattern 1: Edge Gateway + Serverless Backend// Edge: Authentication, routing, rate limiting// Serverless: Business logic, database operations// Edge Gatewayexport const runtime = 'edge';export async function GET(request: Request) {// 1. Authenticate (fast at edge)const user = await authenticate(request);if (!user) return new Response('Unauthorized', { status: 401 });// 2. Rate limit (fast at edge)if (await isRateLimited(user.id)) {return new Response('Rate limited', { status: 429 });}// 3. Route to serverless backendconst backendResponse = await fetch(process.env.BACKEND_URL + '/api/data', {headers: {'X-User-ID': user.id,'Authorization': request.headers.get('Authorization')}});// 4. Transform response at edgeconst data = await backendResponse.json();const transformed = {...data,processed_at: new Date().toISOString(),edge_location: request.headers.get('x-vercel-id')};return Response.json(transformed);}// โ Pattern 2: Edge Caching + Serverless Generation// Edge: Serve cached content// Serverless: Generate and cache content// Edge: Cache layerexport const runtime = 'edge';export async function GET(request: Request) {const cache = caches.default;const cacheKey = request.url;// Try cache firstlet response = await cache.match(cacheKey);if (!response) {// Cache miss: generate at serverlessconst serverlessResponse = await fetch(process.env.SERVERLESS_URL + '/generate',{ method: 'POST', body: JSON.stringify({ url: request.url }) });response = new Response(serverlessResponse.body, {headers: {...Object.fromEntries(serverlessResponse.headers),'Cache-Control': 'public, s-maxage=3600'}});// Cache for future requestsawait cache.put(cacheKey, response.clone());}return response;}// โ Pattern 3: Edge Personalization + Serverless Data// Edge: Personalize content// Serverless: Fetch user dataexport const runtime = 'edge';export async function GET(request: Request) {const user = await getUserFromRequest(request);// Fetch personalized data from serverlessconst dataResponse = await fetch(`${process.env.SERVERLESS_URL}/api/user-data/${user.id}`);const userData = await dataResponse.json();// Personalize at edge (fast)const personalized = {...userData,recommendations: personalizeRecommendations(userData.history,request.geo?.country),localized_content: getLocalizedContent(userData.content,request.headers.get('Accept-Language'))};return Response.json(personalized);}// โ Pattern 4: Edge API Gateway// Single edge function routes to multiple serverless backendsexport const runtime = 'edge';export async function GET(request: Request) {const url = new URL(request.url);const service = url.pathname.split('/')[2]; // /api/{service}/...const serviceMap: Record<string, string> = {'users': process.env.USER_SERVICE_URL,'products': process.env.PRODUCT_SERVICE_URL,'orders': process.env.ORDER_SERVICE_URL,'analytics': process.env.ANALYTICS_SERVICE_URL};const serviceUrl = serviceMap[service];if (!serviceUrl) {return new Response('Service not found', { status: 404 });}// Add edge headersconst headers = new Headers(request.headers);headers.set('X-Edge-Gateway', 'true');headers.set('X-Request-ID', crypto.randomUUID());// Proxy to appropriate serviceconst response = await fetch(`${serviceUrl}${url.pathname}`, {method: request.method,headers,body: request.body});return response;}
6. Edge Computing Best Practices
Learn production-ready patterns, error handling, monitoring, and optimization techniques that senior engineers use to build reliable edge applications.
// โ Best Practice 1: Error Handlingexport const runtime = 'edge';export async function GET(request: Request) {try {const data = await fetchData();return Response.json(data);} catch (error) {// Log error (use external service like Sentry)console.error('Edge function error:', error);// Return user-friendly errorreturn Response.json({ error: 'Service temporarily unavailable' },{ status: 503 });}}// โ Best Practice 2: Timeout Handlingexport const runtime = 'edge';const TIMEOUT_MS = 25000; // Leave buffer before 30s limitexport async function GET(request: Request) {const controller = new AbortController();const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);try {const response = await fetch('https://api.example.com/data', {signal: controller.signal});clearTimeout(timeoutId);return response;} catch (error) {clearTimeout(timeoutId);if (error.name === 'AbortError') {return Response.json({ error: 'Request timeout' },{ status: 504 });}throw error;}}// โ Best Practice 3: Request Validationexport const runtime = 'edge';const schema = {userId: (v: string) => /^[a-zA-Z0-9]+$/.test(v),limit: (v: number) => v > 0 && v <= 100};export async function GET(request: Request) {const url = new URL(request.url);const userId = url.searchParams.get('userId');const limit = parseInt(url.searchParams.get('limit') || '10');// Validate at edge (fast)if (!userId || !schema.userId(userId)) {return Response.json({ error: 'Invalid userId' },{ status: 400 });}if (!schema.limit(limit)) {return Response.json({ error: 'Limit must be between 1 and 100' },{ status: 400 });}// Proceed with validated requestconst data = await fetchUserData(userId, limit);return Response.json(data);}// โ Best Practice 4: Caching with Invalidationexport const runtime = 'edge';export async function GET(request: Request) {const cache = caches.default;const cacheKey = request.url;// Check cacheconst cached = await cache.match(cacheKey);if (cached) {const age = parseInt(cached.headers.get('age') || '0');if (age < 3600) { // 1 hourreturn cached;}}// Fetch freshconst data = await fetchFreshData();const response = Response.json(data, {headers: {'Cache-Control': 'public, s-maxage=3600','Age': '0'}});await cache.put(cacheKey, response.clone());return response;}// โ Best Practice 5: Monitoring & Observabilityexport const runtime = 'edge';export async function GET(request: Request) {const startTime = Date.now();const requestId = crypto.randomUUID();try {const data = await fetchData();const duration = Date.now() - startTime;// Log metrics (send to analytics)logMetrics({requestId,duration,status: 200,endpoint: new URL(request.url).pathname});return Response.json(data, {headers: {'X-Request-ID': requestId,'X-Response-Time': `${duration}ms`}});} catch (error) {const duration = Date.now() - startTime;logMetrics({requestId,duration,status: 500,error: error.message,endpoint: new URL(request.url).pathname});throw error;}}// โ Best Practice 6: Security Headersexport const runtime = 'edge';export async function GET(request: Request) {const data = await fetchData();return Response.json(data, {headers: {'Content-Type': 'application/json','X-Content-Type-Options': 'nosniff','X-Frame-Options': 'DENY','X-XSS-Protection': '1; mode=block','Strict-Transport-Security': 'max-age=31536000; includeSubDomains','Content-Security-Policy': "default-src 'self'"}});}// โ Best Practice 7: Request Deduplicationconst pendingRequests = new Map<string, Promise<Response>>();export const runtime = 'edge';export async function GET(request: Request) {const cacheKey = request.url;// If same request is pending, wait for itif (pendingRequests.has(cacheKey)) {return pendingRequests.get(cacheKey)!;}// Create new requestconst promise = (async () => {try {const data = await fetchData();const response = Response.json(data);return response;} finally {pendingRequests.delete(cacheKey);}})();pendingRequests.set(cacheKey, promise);return promise;}// โ Best Practice 8: Graceful Degradationexport const runtime = 'edge';export async function GET(request: Request) {try {// Try primary data sourceconst data = await fetchPrimaryData();return Response.json(data);} catch (error) {console.error('Primary source failed:', error);try {// Fallback to secondary sourceconst fallbackData = await fetchFallbackData();return Response.json({...fallbackData,_fallback: true,_cached: true});} catch (fallbackError) {// Last resort: return cached/stale dataconst cached = await getCachedData();if (cached) {return Response.json({...cached,_stale: true,_warning: 'Service degraded, showing cached data'});}// Complete failurereturn Response.json({ error: 'Service unavailable' },{ status: 503 });}}}
Production Checklist:
โ
Implement proper error handling and logging
โ
Set appropriate timeouts (leave buffer before limits)
โ
Validate requests at the edge
โ
Implement caching strategies
โ
Add monitoring and observability
โ
Set security headers
โ
Handle request deduplication
โ
Implement graceful degradation
โ
Test cold start scenarios
โ
Monitor edge function performance
โ
Set up alerting for errors
โ
Document edge function behavior