Skip to main content
← All Articles · · 12 min read

Serverless Edge Computing 2026: Vercel, Cloudflare Workers, Deno Deploy Compared

Compare Vercel Edge Functions, Cloudflare Workers, and Deno Deploy for 2026. Performance benchmarks, pricing, and when to use each platform.

edgeserverlesscloudflare-workersverceldeno-deploy

Edge computing has moved from a niche optimization trick to a mainstream deployment strategy. In 2026, the three dominant platforms — Vercel Edge Functions, Cloudflare Workers, and Deno Deploy — each offer distinct tradeoffs around performance, pricing, ecosystem, and developer experience. This guide cuts through the marketing to show you exactly what each platform does well, where it falls short, and how to choose.

What Edge Computing Actually Means (and Why Cold Starts Matter)

Traditional serverless functions like AWS Lambda run in a single region. When a user in Singapore hits your US-East-1 Lambda, the request travels across the planet before getting processed. Round-trip latency stacks up: DNS lookup, TCP handshake, TLS negotiation, cold start, execution, response. On a cold Lambda, you’re easily looking at 800ms–2s before the first byte hits the browser.

Edge functions flip this model. Instead of running your code in one region, the platform deploys it to 100+ points of presence (PoPs) worldwide. A user in Singapore hits a data center in Singapore. Network latency drops from 150–300ms to under 20ms.

The cold start story is even more dramatic. Traditional Lambda cold starts range from 200ms to over 1s depending on runtime and memory. Edge runtimes use V8 isolates instead of full container sandboxes. An isolate spins up in under 5ms — often under 1ms. This is because isolates share a V8 process; only your code is isolated, not the entire OS-level container.

The constraint: edge runtimes are intentionally limited. No fs module, no arbitrary npm packages that use Node.js native APIs, CPU time limits of 5–50ms per request, and memory caps of 128MB–256MB. You cannot run long-running computation, database migrations, or video encoding at the edge. Edge is for fast, stateless request transformation.


Vercel Edge Functions vs Edge Middleware

Vercel gives you two edge primitives that serve different purposes. Conflating them is one of the most common mistakes developers make.

Edge Middleware

Middleware runs before your route handlers on every matched request. It cannot return a full response body — it can only rewrite, redirect, add headers, or modify the request. It runs on the Vercel Edge Network using the V8-based Edge Runtime.

// middleware.ts (runs at edge before all routes)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const country = request.geo?.country ?? 'US'
  const url = request.nextUrl.clone()

  // Geo-redirect: send EU users to GDPR-compliant variant
  if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(country)) {
    url.pathname = `/eu${url.pathname}`
    return NextResponse.rewrite(url)
  }

  // A/B test: assign bucket via cookie, persist across requests
  const bucket = request.cookies.get('ab-bucket')?.value
  if (!bucket) {
    const assigned = Math.random() < 0.5 ? 'control' : 'variant'
    const response = NextResponse.next()
    response.cookies.set('ab-bucket', assigned, { maxAge: 60 * 60 * 24 * 7 })
    return response
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Middleware executes on every request that matches the pattern. Keep it fast — you’re adding latency to every page load. Avoid fetching from external APIs inside middleware unless you cache aggressively.

Edge Functions

Edge Functions are full route handlers that run at the edge and can return complete responses. In Next.js App Router, you opt in per route with export const runtime = 'edge'.

// app/api/personalize/route.ts
export const runtime = 'edge'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const userId = searchParams.get('userId')

  // Edge KV lookup via Vercel KV (Redis-compatible)
  const { kv } = await import('@vercel/kv')
  const prefs = await kv.get<UserPreferences>(`prefs:${userId}`)

  return Response.json({
    theme: prefs?.theme ?? 'light',
    locale: prefs?.locale ?? 'en',
    recommendations: prefs?.topCategories ?? [],
  })
}

Use Edge Functions when you need to return a full response and benefit from low-latency global distribution — personalization APIs, auth token validation, feature flag evaluation. Use Edge Middleware when you need to intercept and transform requests transparently without the caller knowing.


Cloudflare Workers: The Fullest Edge Platform

Cloudflare Workers is the most mature edge platform with the deepest storage and compute ecosystem. Workers run on Cloudflare’s network of 300+ PoPs and consistently deliver sub-1ms cold starts.

Basic Worker with KV

Cloudflare KV is a globally replicated key-value store with eventual consistency. Reads are served from the local PoP; writes propagate within ~60 seconds. Ideal for configuration, feature flags, and cached data that does not need strict consistency.

// worker.js
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url)
    const cacheKey = `page:${url.pathname}`

    // Try KV cache first
    const cached = await env.PAGE_CACHE.get(cacheKey, { type: 'text' })
    if (cached) {
      return new Response(cached, {
        headers: { 'Content-Type': 'text/html', 'X-Cache': 'HIT' },
      })
    }

    // Fetch from origin
    const origin = await fetch(`https://api.example.com${url.pathname}`)
    const html = await origin.text()

    // Cache with TTL, non-blocking via ctx.waitUntil
    ctx.waitUntil(env.PAGE_CACHE.put(cacheKey, html, { expirationTtl: 300 }))

    return new Response(html, {
      headers: { 'Content-Type': 'text/html', 'X-Cache': 'MISS' },
    })
  },
}

Durable Objects: Stateful Edge

Durable Objects are Cloudflare’s answer to the statelessness problem. Each Durable Object is a singleton that lives in one location, provides strongly consistent storage, and can hold in-memory state between requests that hit the same object. This enables use cases like rate limiting, real-time collaboration, and session management that are impossible with pure stateless edge.

// Durable Object for per-user rate limiting
export class RateLimiter {
  private state: DurableObjectState
  private requests: number = 0
  private windowStart: number = Date.now()

  constructor(state: DurableObjectState) {
    this.state = state
  }

  async fetch(request: Request): Promise<Response> {
    const now = Date.now()
    const windowMs = 60_000 // 1 minute window
    const limit = 100

    if (now - this.windowStart > windowMs) {
      this.requests = 0
      this.windowStart = now
    }

    this.requests++

    if (this.requests > limit) {
      return new Response('Rate limit exceeded', {
        status: 429,
        headers: { 'Retry-After': String(Math.ceil((this.windowStart + windowMs - now) / 1000)) },
      })
    }

    return new Response(JSON.stringify({ remaining: limit - this.requests }), {
      headers: { 'Content-Type': 'application/json' },
    })
  }
}

// Main worker routing to Durable Object
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const ip = request.headers.get('CF-Connecting-IP') ?? 'unknown'
    const id = env.RATE_LIMITER.idFromName(ip)
    const limiter = env.RATE_LIMITER.get(id)
    return limiter.fetch(request)
  },
}

R2 and D1

R2 is Cloudflare’s S3-compatible object storage with zero egress fees — a significant cost advantage over AWS S3 for high-traffic assets. D1 is Cloudflare’s SQLite-based SQL database, replicated globally with read replicas at each PoP.

// Serving files from R2 with cache headers
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url)
    const key = url.pathname.slice(1) // strip leading /

    const object = await env.ASSETS_BUCKET.get(key)
    if (!object) return new Response('Not Found', { status: 404 })

    const headers = new Headers()
    object.writeHttpMetadata(headers)
    headers.set('Cache-Control', 'public, max-age=31536000, immutable')
    headers.set('ETag', object.etag)

    return new Response(object.body, { headers })
  },
}
// D1 query at edge — no connection pooling needed
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { results } = await env.DB.prepare(
      'SELECT id, title, slug FROM posts WHERE published = 1 ORDER BY created_at DESC LIMIT ?'
    ).bind(10).all()

    return Response.json(results)
  },
}

Deno Deploy: TypeScript-First Edge

Deno Deploy runs Deno isolates on a global network. The core differentiation: native TypeScript support (no build step), a strict security model (explicit permission grants), and Deno KV — a strongly consistent key-value store built into the runtime.

Basic Deno Deploy Function

// main.ts — deploy with: deployctl deploy main.ts
Deno.serve(async (req: Request): Promise<Response> => {
  const url = new URL(req.url)

  if (url.pathname === '/api/health') {
    return Response.json({ status: 'ok', region: Deno.env.get('DENO_REGION') })
  }

  if (url.pathname === '/api/user' && req.method === 'GET') {
    const userId = url.searchParams.get('id')
    if (!userId) return new Response('Missing id', { status: 400 })

    const kv = await Deno.openKv()
    const user = await kv.get(['users', userId])

    if (!user.value) return new Response('Not Found', { status: 404 })
    return Response.json(user.value)
  }

  return new Response('Not Found', { status: 404 })
})

Deno KV: Strongly Consistent Global Storage

Unlike Cloudflare KV’s eventual consistency, Deno KV offers strong consistency for reads and writes. This makes it suitable for financial data, inventory counts, and any case where stale reads would cause correctness issues.

// Atomic counter with Deno KV — race-condition safe
const kv = await Deno.openKv()

async function incrementPageView(slug: string): Promise<number> {
  const key = ['pageviews', slug]

  while (true) {
    const current = await kv.get<number>(key)
    const newCount = (current.value ?? 0) + 1

    const result = await kv.atomic()
      .check(current) // optimistic concurrency: only commit if value hasn't changed
      .set(key, newCount)
      .commit()

    if (result.ok) return newCount
    // If check fails (concurrent write), retry
  }
}

Deno.serve(async (req) => {
  const url = new URL(req.url)
  const slug = url.searchParams.get('slug') ?? 'home'
  const views = await incrementPageView(slug)
  return Response.json({ slug, views })
})

Deno Deploy’s TypeScript support means you get type checking without a compilation step. You push .ts files directly. This simplifies CI/CD and eliminates an entire class of “works in dev, broken in prod” build failures.


Feature Comparison Table

FeatureVercel EdgeCloudflare WorkersDeno Deploy
Cold Start~0ms (warm) / ~5ms<1ms~1–5ms
CPU Limit5ms (Hobby) / 15ms (Pro)10ms (free) / 30ms (paid)50ms
Memory Limit128MB128MB512MB
Max Request Duration30s (streaming)30s (paid)60s
Storage (KV)Vercel KV (Redis)KV (eventual)Deno KV (strong)
SQL DatabaseVercel PostgresD1 (SQLite)- (bring your own)
Object StorageVercel BlobR2 (S3-compat)-
Stateful Compute-Durable Objects-
TypeScript NativeNo (compile)No (wrangler builds)Yes
npm CompatibilityMost packagesMost packagesnpm: specifiers
Free Tier Requests100k/day100k/day100k/day
Paid Pricing$20/mo (Pro)$5/mo + usage$10/mo + usage
Egress FeesYesNo (R2)Minimal
PoPs~100300+35+
RuntimeV8 (Edge Runtime)V8 (workerd)V8 (Deno)

Real Use Cases with Code

Geo-Routing (Cloudflare Workers)

export default {
  async fetch(request, env) {
    const country = request.cf?.country ?? 'US'
    const continent = request.cf?.continent ?? 'NA'

    const regionMap = {
      EU: 'https://eu.api.example.com',
      AS: 'https://ap.api.example.com',
      NA: 'https://us.api.example.com',
    }

    const origin = regionMap[continent] ?? regionMap.NA
    const url = new URL(request.url)
    const targetUrl = `${origin}${url.pathname}${url.search}`

    const response = await fetch(targetUrl, {
      method: request.method,
      headers: request.headers,
      body: request.body,
    })

    return new Response(response.body, {
      status: response.status,
      headers: {
        ...Object.fromEntries(response.headers),
        'X-Served-From': origin,
        'X-User-Country': country,
      },
    })
  },
}

Auth Validation at Edge (Vercel Middleware)

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify, importSPKI } from 'jose'

const PUBLIC_KEY = process.env.JWT_PUBLIC_KEY!

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl

  // Skip public routes
  if (pathname.startsWith('/public') || pathname === '/login') {
    return NextResponse.next()
  }

  const token = request.cookies.get('auth-token')?.value
    ?? request.headers.get('Authorization')?.replace('Bearer ', '')

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  try {
    const publicKey = await importSPKI(PUBLIC_KEY, 'RS256')
    const { payload } = await jwtVerify(token, publicKey)

    // Inject user info into headers for downstream handlers
    const requestHeaders = new Headers(request.headers)
    requestHeaders.set('X-User-Id', payload.sub ?? '')
    requestHeaders.set('X-User-Role', String(payload.role ?? 'user'))

    return NextResponse.next({ request: { headers: requestHeaders } })
  } catch {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

export const config = { matcher: ['/dashboard/:path*', '/api/protected/:path*'] }

Personalization with Deno KV

// Edge personalization — returns tailored content based on user history
const kv = await Deno.openKv()

interface UserProfile {
  segments: string[]
  lastSeen: number
  preferredLang: string
}

Deno.serve(async (req: Request): Promise<Response> => {
  const userId = req.headers.get('X-User-Id')
  if (!userId) return Response.json({ variant: 'default' })

  const profile = await kv.get<UserProfile>(['profiles', userId])
  const segments = profile.value?.segments ?? []

  // Update last seen asynchronously
  const updated: UserProfile = {
    segments,
    lastSeen: Date.now(),
    preferredLang: req.headers.get('Accept-Language')?.split(',')[0] ?? 'en',
  }
  // Fire and forget — don't block the response
  kv.set(['profiles', userId], updated).catch(console.error)

  const variant = segments.includes('premium')
    ? 'premium'
    : segments.includes('trial')
    ? 'trial'
    : 'free'

  return Response.json({
    variant,
    lang: updated.preferredLang,
    features: getFeatureFlags(variant),
  })
})

function getFeatureFlags(variant: string) {
  const flags: Record<string, string[]> = {
    premium: ['advanced-analytics', 'priority-support', 'export-csv'],
    trial: ['advanced-analytics'],
    free: [],
  }
  return flags[variant] ?? []
}

Migration Guide: Lambda to Edge

Moving from AWS Lambda (or Vercel Serverless Functions) to edge functions requires understanding what breaks and what you need to replace.

What breaks at edge:

  • Node.js built-in modules: fs, path, child_process, crypto (use globalThis.crypto instead)
  • Long-running processes or background tasks beyond 30–50ms CPU
  • Native Node addons (.node files)
  • Large npm packages with heavy native dependencies (Prisma ORM, sharp image processing)

Step 1: Audit your dependencies

# Check which packages use Node.js APIs not available at edge
npx edge-runtime-compat check ./src/app/api

Step 2: Replace storage

// Before: Prisma + PostgreSQL (not edge-compatible)
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const user = await prisma.user.findUnique({ where: { id } })

// After: Prisma Accelerate (edge-compatible connection pooler) or Drizzle + Neon HTTP
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL!)
const [user] = await sql`SELECT * FROM users WHERE id = ${id} LIMIT 1`

Step 3: Replace Node crypto

// Before: Node.js crypto
import crypto from 'crypto'
const hash = crypto.createHash('sha256').update(data).digest('hex')

// After: Web Crypto API (works everywhere)
const encoder = new TextEncoder()
const buffer = await globalThis.crypto.subtle.digest('SHA-256', encoder.encode(data))
const hash = Array.from(new Uint8Array(buffer))
  .map(b => b.toString(16).padStart(2, '0'))
  .join('')

Step 4: Handle streaming responses

Edge functions can stream responses using ReadableStream, enabling progressive HTML rendering and real-time data without holding the connection open for the full execution time.

export const runtime = 'edge'

export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder()

      for (const chunk of ['First chunk\n', 'Second chunk\n', 'Done\n']) {
        controller.enqueue(encoder.encode(chunk))
        await new Promise(r => setTimeout(r, 100)) // simulate async work
      }
      controller.close()
    },
  })

  return new Response(stream, {
    headers: { 'Content-Type': 'text/plain', 'Transfer-Encoding': 'chunked' },
  })
}

Which Platform Should You Choose?

Choose Vercel Edge Functions if you are already on Vercel and primarily need low-latency middleware (auth, redirects, A/B testing) tightly integrated with Next.js or SvelteKit. The developer experience is unmatched for full-stack JavaScript apps, and Vercel KV / Blob storage integrate without configuration.

Choose Cloudflare Workers if you need the deepest platform capabilities: Durable Objects for stateful edge logic, R2 for zero-egress storage, D1 for SQL at edge, or if you are building infrastructure-level tooling that is not tied to a specific framework. Cloudflare’s 300+ PoP network also means the most consistent global latency.

Choose Deno Deploy if TypeScript-first development is a priority, you want strong consistency in KV storage without complexity, or your team prefers Deno’s explicit permission model and web-standard APIs. Deno Deploy has fewer PoPs than Cloudflare but is actively expanding. The lack of a build step is a genuine productivity advantage for smaller teams.

All three platforms are production-ready in 2026. The decision usually comes down to ecosystem fit rather than raw capability — if your project already lives in one of these ecosystems, stay there and learn it deeply before jumping platforms.

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.