MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Vercel Edge Functions: Serverless at the Edge

Deploy serverless functions at Vercel's edge: configuration, limitations, and use cases.

VercelEdge FunctionsServerlessFrontend

By MinhVo

Introduction

Edge computing is transforming how we build web applications. Instead of routing every request to a centralized server in a single region, edge functions execute your code at the network edge—on servers geographically closest to your users. Vercel Edge Functions leverage this paradigm by running your code on Cloudflare's global network of 300+ data centers, delivering sub-50ms response times regardless of where your users are located.

Unlike traditional serverless functions that execute in a single region (typically US-East), edge functions are distributed globally. This means a user in Tokyo gets the same fast response as a user in New York. Vercel Edge Functions are built on the Web API standard, making them familiar to frontend developers who already work with fetch, Request, Response, and URL objects.

In this comprehensive guide, we'll explore how Vercel Edge Functions work, when to use them versus traditional serverless functions, their limitations, and practical implementation patterns for authentication, A/B testing, geolocation-based content, and more.

Edge Computing

Understanding Edge Functions: Core Concepts

Edge Functions represent a fundamental shift from the traditional request-response model. Understanding their architecture and constraints is essential for using them effectively.

How Edge Functions Work

When a user makes a request to your Vercel deployment, the request is routed to the nearest edge location. If you have an Edge Function configured for that route, it executes there—before the request reaches your origin server. The Edge Function can modify the request, return a response, redirect the user, or pass the request through to your application.

Web Standard APIs

Edge Functions use the Web API standard (WinterCG), not Node.js APIs. This means you have access to:

  • fetch for making HTTP requests
  • Request and Response for handling HTTP
  • URL and URLSearchParams for URL manipulation
  • TextEncoder/TextDecoder for string encoding
  • crypto for cryptographic operations
  • ReadableStream/WritableStream for streaming

You do NOT have access to:

  • Node.js fs module (no filesystem)
  • Node.js http/https modules
  • Most npm packages that depend on Node.js APIs
  • Long-running processes (execution is limited to 30 seconds)

Edge vs. Serverless vs. Server

Understanding when to use each compute option is critical:

  • Edge Functions: Best for lightweight, latency-sensitive operations (auth, redirects, A/B tests, geolocation). Limited runtime, no Node.js APIs.
  • Serverless Functions (Vercel): Full Node.js runtime, access to databases and file systems. Regional execution (typically US-East).
  • Server (Vercel KV, Vercel Postgres): Persistent connections, full database access. Use when you need connection pooling or transactions.

Serverless Architecture

Architecture and Design Patterns

Middleware Pattern

The most common Edge Function pattern is middleware—code that runs before your application handles the request. In Next.js, this is implemented using middleware.ts at the root of your project:

// middleware.ts (Next.js)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
 
export function middleware(request: NextRequest) {
  // Runs on the edge for every matched route
  const token = request.cookies.get('auth-token');
  
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  
  return NextResponse.next();
}
 
export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

Edge API Routes

For standalone Edge Functions (not middleware), create files in the app/api directory with the runtime export:

// app/api/hello/route.ts
export const runtime = 'edge';
 
export async function GET(request: Request) {
  return new Response(
    JSON.stringify({ message: 'Hello from the edge!' }),
    {
      headers: { 'Content-Type': 'application/json' },
    }
  );
}

Geolocation Pattern

Edge Functions have access to geographic information about the request, enabling location-based content delivery:

export const runtime = 'edge';
 
export async function GET(request: Request) {
  const country = request.headers.get('x-vercel-ip-country') || 'US';
  const city = request.headers.get('x-vercel-ip-city') || 'Unknown';
  const latitude = request.headers.get('x-vercel-ip-latitude');
  const longitude = request.headers.get('x-vercel-ip-longitude');
 
  // Serve region-specific content
  const content = getContentForRegion(country);
  
  return new Response(JSON.stringify({ country, city, latitude, longitude, content }), {
    headers: { 'Content-Type': 'application/json' },
  });
}

Global Network

Step-by-Step Implementation

Setting Up Edge Functions in Next.js

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Edge Functions are automatically available in Next.js 13+
  // No special configuration needed
};
 
module.exports = nextConfig;

Authentication Middleware

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyToken } from './lib/auth-edge';
 
// Edge-compatible JWT verification (no Node.js crypto)
async function verifyJwt(token: string, secret: string) {
  const [header, payload, signature] = token.split('.');
  const encoder = new TextEncoder();
  
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['verify']
  );
  
  const valid = await crypto.subtle.verify(
    'HMAC',
    key,
    Uint8Array.from(atob(signature), c => c.charCodeAt(0)),
    encoder.encode(`${header}.${payload}`)
  );
  
  if (!valid) throw new Error('Invalid signature');
  return JSON.parse(atob(payload));
}
 
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  // Public routes
  if (pathname.startsWith('/login') || pathname.startsWith('/register') || pathname === '/') {
    return NextResponse.next();
  }
 
  // Protected routes
  const token = request.cookies.get('auth-token')?.value;
  
  if (!token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    return NextResponse.redirect(loginUrl);
  }
 
  try {
    const payload = await verifyJwt(token, process.env.JWT_SECRET!);
    
    // Add user info to headers for downstream use
    const response = NextResponse.next();
    response.headers.set('x-user-id', payload.sub);
    response.headers.set('x-user-role', payload.role);
    
    // Role-based access control
    if (pathname.startsWith('/admin') && payload.role !== 'admin') {
      return NextResponse.redirect(new URL('/unauthorized', request.url));
    }
    
    return response;
  } catch {
    // Invalid token—clear cookie and redirect
    const response = NextResponse.redirect(new URL('/login', request.url));
    response.cookies.delete('auth-token');
    return response;
  }
}
 
export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*', '/api/protected/:path*'],
};

A/B Testing at the Edge

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
 
const AB_TESTS = {
  'homepage-hero': {
    variants: ['control', 'variant-a', 'variant-b'],
    weights: [0.34, 0.33, 0.33],
  },
  'pricing-layout': {
    variants: ['grid', 'table'],
    weights: [0.5, 0.5],
  },
};
 
function selectVariant(testName: string, userId: string): string {
  const test = AB_TESTS[testName];
  const hash = Array.from(userId + testName).reduce(
    (acc, char) => acc + char.charCodeAt(0), 0
  );
  const normalized = (hash % 1000) / 1000;
  
  let cumulative = 0;
  for (let i = 0; i < test.variants.length; i++) {
    cumulative += test.weights[i];
    if (normalized < cumulative) return test.variants[i];
  }
  return test.variants[0];
}
 
export function middleware(request: NextRequest) {
  const userId = request.cookies.get('user-id')?.value || 
    crypto.randomUUID();
  
  const response = NextResponse.next();
  
  // Set user ID cookie if not present
  if (!request.cookies.get('user-id')) {
    response.cookies.set('user-id', userId, { 
      httpOnly: true, 
      secure: true,
      sameSite: 'lax',
      maxAge: 365 * 24 * 60 * 60,
    });
  }
  
  // Assign A/B test variants
  for (const testName of Object.keys(AB_TESTS)) {
    const variant = selectVariant(testName, userId);
    response.headers.set(`x-ab-${testName}`, variant);
    response.cookies.set(`ab-${testName}`, variant, {
      httpOnly: false, // Client needs to read this
      secure: true,
      sameSite: 'lax',
      maxAge: 30 * 24 * 60 * 60,
    });
  }
  
  return response;
}
 
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

Rate Limiting at the Edge

// app/api/protected/route.ts
export const runtime = 'edge';
 
// Simple in-memory rate limiter (resets on cold start)
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
 
function checkRateLimit(ip: string, limit: number, windowMs: number): boolean {
  const now = Date.now();
  const entry = rateLimitMap.get(ip);
  
  if (!entry || now > entry.resetTime) {
    rateLimitMap.set(ip, { count: 1, resetTime: now + windowMs });
    return true;
  }
  
  if (entry.count >= limit) {
    return false;
  }
  
  entry.count++;
  return true;
}
 
export async function GET(request: Request) {
  const ip = request.headers.get('x-forwarded-for') || 'unknown';
  
  if (!checkRateLimit(ip, 100, 60000)) {
    return new Response('Too Many Requests', { 
      status: 429,
      headers: { 'Retry-After': '60' },
    });
  }
  
  return new Response(JSON.stringify({ message: 'OK' }), {
    headers: { 'Content-Type': 'application/json' },
  });
}

Content Personalization

// app/page.tsx (using edge headers in a Server Component)
import { headers } from 'next/headers';
 
export default async function HomePage() {
  const headersList = headers();
  const country = headersList.get('x-vercel-ip-country') || 'US';
  const language = headersList.get('accept-language')?.split(',')[0] || 'en';
  
  // Fetch region-specific content
  const content = await getRegionalContent(country, language);
  
  return (
    <main>
      <h1>{content.title}</h1>
      <p>{content.description}</p>
      {country === 'JP' && <JapanPromotionBanner />}
      {country === 'DE' && <EUPrivacyNotice />}
    </main>
  );
}

Real-World Use Cases and Case Studies

Use Case 1: Authentication Gateway

A SaaS platform uses Edge Functions as an authentication gateway. Every request to protected routes passes through the edge middleware, which verifies JWT tokens and checks permissions. This eliminates the need for authentication logic in every API route and ensures consistent security across the application. The edge execution means authentication adds less than 5ms to request latency.

Use Case 2: Feature Flags and Progressive Rollouts

A startup uses Edge Functions to implement feature flags. New features are rolled out progressively—first to internal users, then to 10% of traffic, then 50%, then 100%. The edge middleware reads feature flag configurations from Vercel KV (a Redis-compatible edge store) and injects the appropriate flags into request headers. This allows instant rollbacks without redeployment.

Use Case 3: Bot Detection and Security

An e-commerce site uses Edge Functions to detect and block malicious bots before they reach the application. The edge function analyzes request headers, user agent strings, and behavioral patterns. Suspicious requests are blocked with a 403 response, while legitimate traffic passes through. This reduces server load by 40% and prevents scraping attacks.

Use Case 4: Internationalization Routing

A global media company uses Edge Functions to route users to localized versions of their site. Based on the Accept-Language header and geolocation data, the edge function rewrites URLs to include the appropriate locale prefix (/en/, /ja/, /de/). This happens transparently—users always see content in their preferred language.

Best Practices for Production

  1. Keep edge functions lightweight: Edge Functions have a 4MB bundle size limit and 30-second execution limit. Avoid heavy computation and large dependencies.

  2. Use Web APIs, not Node.js APIs: Edge Functions run on the WinterCG standard. Use fetch instead of axios, crypto.subtle instead of Node's crypto, and TextEncoder instead of Buffer.

  3. Implement proper error handling: Edge Functions should never crash. Always wrap code in try-catch and return meaningful error responses.

  4. Cache aggressively: Use Cache-Control headers and Vercel's edge caching to minimize origin requests. Edge Functions can manipulate cache headers for fine-grained control.

  5. Minimize cold starts: Keep your edge function code small and avoid dynamic imports. The smaller the bundle, the faster the cold start.

  6. Use environment variables correctly: Access environment variables via process.env (they're available at the edge). Don't hardcode secrets.

  7. Monitor edge function performance: Use Vercel's built-in analytics to track edge function execution times, error rates, and invocation counts.

  8. Test locally with vercel dev: Vercel's CLI simulates the edge runtime locally, so you can test edge functions before deploying.

Common Pitfalls and Solutions

PitfallImpactSolution
Using Node.js-specific APIsBuild errors, runtime crashesUse Web API alternatives (fetch, crypto.subtle, etc.)
Large bundle sizesSlow cold starts, deployment failuresTree-shake dependencies, avoid large libraries
No error handling500 errors for usersWrap all code in try-catch, return meaningful errors
Assuming persistent stateInconsistent behaviorEdge functions are stateless; use external stores (KV, database)
Ignoring geo-headersMissing personalization opportunitiesRead x-vercel-ip-* headers for location data
Overusing edge for heavy computationTimeouts, poor performanceUse serverless functions for CPU-intensive tasks

Performance Optimization

Edge Functions inherently provide performance benefits through geographic distribution, but you can optimize further:

// Optimize: Use streaming for large responses
export const runtime = 'edge';
 
export async function GET(request: Request) {
  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder();
      
      // Stream data as it's available
      for await (const chunk of dataStream) {
        controller.enqueue(encoder.encode(JSON.stringify(chunk)));
      }
      
      controller.close();
    },
  });
 
  return new Response(stream, {
    headers: { 'Content-Type': 'application/json' },
  });
}
 
// Optimize: Edge-side caching
export async function GET(request: Request) {
  const cacheKey = new Request(request.url, request);
  const cache = caches.default;
  
  let response = await cache.match(cacheKey);
  
  if (!response) {
    response = await fetchFromOrigin(request);
    response = new Response(response.body, response);
    response.headers.set('Cache-Control', 's-maxage=3600, stale-while-revalidate=86400');
    await cache.put(cacheKey, response.clone());
  }
  
  return response;
}

Comparison with Alternatives

FeatureVercel Edge FunctionsCloudflare WorkersAWS Lambda@EdgeDeno Deploy
RuntimeEdge (V8)Edge (V8)Edge (Node.js)Edge (V8)
Cold Start<5ms<5ms50-200ms<10ms
Max Execution30s30s (10ms billing)30s50ms-60s
Bundle Size4MB10MB (after compression)1MB (viewer)10MB
Web APIsFull WinterCGFull WinterCGLimitedFull WinterCG
Database AccessVercel KV, PostgresD1, KV, HyperdriveRDS, DynamoDBKV, Postgres
PricingPer invocationPer requestPer invocationPer request
Best ForNext.js appsGeneral edge computeAWS ecosystemDeno/TypeScript

Advanced Patterns

Edge-Side Rendering with Streaming

// app/products/[id]/page.tsx
export const runtime = 'edge';
 
export default async function ProductPage({ params }: { params: { id: string } }) {
  // Stream product data from edge
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    headers: { 'Cache-Control': 'max-age=300' },
  }).then(r => r.json());
 
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={params.id} />
      </Suspense>
    </div>
  );
}

Edge-Optimized Session Management

// lib/session-edge.ts
export class EdgeSession {
  private kv: KVNamespace;
 
  constructor(kv: KVNamespace) {
    this.kv = kv;
  }
 
  async get(sessionId: string): Promise<SessionData | null> {
    const data = await this.kv.get(`session:${sessionId}`, 'json');
    return data as SessionData | null;
  }
 
  async set(sessionId: string, data: SessionData, ttlSeconds: number = 3600) {
    await this.kv.put(`session:${sessionId}`, JSON.stringify(data), {
      expirationTtl: ttlSeconds,
    });
  }
 
  async destroy(sessionId: string) {
    await this.kv.delete(`session:${sessionId}`);
  }
}

Testing Strategies

// Test edge middleware locally
import { middleware } from './middleware';
import { NextRequest } from 'next/server';
 
describe('Edge Middleware', () => {
  test('should redirect unauthenticated users to login', () => {
    const request = new NextRequest('http://localhost:3000/dashboard');
    const response = middleware(request);
    
    expect(response.status).toBe(307);
    expect(response.headers.get('location')).toContain('/login');
  });
 
  test('should pass through authenticated requests', () => {
    const request = new NextRequest('http://localhost:3000/dashboard', {
      headers: { cookie: 'auth-token=valid-jwt' },
    });
    const response = middleware(request);
    
    expect(response.status).toBe(200);
  });
});

Future Outlook

Edge computing is becoming the default deployment model for web applications. Vercel continues to expand edge capabilities with:

  • Edge Config: Ultra-low latency configuration store
  • Edge Middleware: Enhanced middleware capabilities
  • Edge Runtime improvements: Better Node.js API compatibility
  • Edge-native databases: Vercel KV and Postgres at the edge

The line between edge and serverless is blurring as runtimes become more capable and databases move closer to users.

Conclusion

Vercel Edge Functions bring your code closer to your users, delivering faster responses and better experiences globally. They're ideal for authentication, personalization, A/B testing, and other latency-sensitive operations.

Key takeaways:

  1. Use edge functions for lightweight, latency-sensitive operations
  2. Use Web APIs, not Node.js APIs—the WinterCG standard is your interface
  3. Keep edge functions small and fast—under 4MB, under 30 seconds
  4. Combine edge middleware with serverless functions for full-stack edge architecture
  5. Cache aggressively at the edge to minimize origin requests

Start by identifying the routes in your application that would benefit most from reduced latency, and move that logic to the edge. The performance improvement is often dramatic—sub-50ms responses from anywhere in the world.