BlogCloud & Infrastructure
Cloud & Infrastructure

Cloudflare Workers vs AWS Lambda: Edge Computing Deep Dive and Decision Framework

Edge computing has matured. Cloudflare Workers runs at 300+ PoPs with sub-millisecond cold starts. AWS Lambda@Edge runs your existing Node.js at CloudFront edge. This guide benchmarks both, covers real-world use cases, and provides a decision framework for choosing between edge runtimes.

A

Alex Thompson

CEO & Cloud Architecture Expert at ZeonEdge with 15+ years building enterprise infrastructure.

March 23, 2026
22 min read

The Edge Computing Landscape in 2026

The promise of edge computing β€” running code geographically close to users β€” has solidified into reality. Cloudflare Workers processes over 50 million requests per second across 300+ data centers. AWS Lambda@Edge and CloudFront Functions execute billions of invocations monthly. Vercel, Netlify, and Deno Deploy have built entire platforms on edge runtimes.

But "edge" means different things to different platforms, and the right choice depends heavily on your use case, existing infrastructure, and performance requirements. This guide compares Cloudflare Workers and AWS Lambda (including Lambda@Edge) across the dimensions that actually matter: cold start latency, execution limits, pricing, ecosystem, and the cases where each wins decisively.

Architecture Comparison

Cloudflare Workers Architecture:
  User Request β†’ Cloudflare Edge PoP (~300 locations)
                      ↓
              Isolate (V8 isolate, not container)
              - Starts in <1ms (no container spinup)
              - CPU limit: 30ms (free) / 30s+ (paid)
              - Memory: 128MB
              - Runtime: V8 (JS/TS/WASM)
                      ↓
              Response (often <5ms total from edge)

AWS Lambda Architecture:
  User Request β†’ AWS API Gateway / CloudFront
                      ↓
              Lambda Function (Linux container/microVM)
              - Cold start: 100ms-3s (language dependent)
              - Warm: <1ms overhead (container reuse)
              - CPU: up to 10 vCPU (configurable by memory)
              - Memory: 128MB-10GB
              - Timeout: 15 minutes
              - Runtimes: Node.js, Python, Java, Go, .NET, Ruby, Custom
                      ↓
              Response (regional, 1-3 AWS regions globally)

Lambda@Edge:
  Same as Lambda but runs at CloudFront edge locations (~450 PoPs)
  - CPU limit: 10 seconds
  - Memory: 128MB-10GB
  - Less flexibility than full Lambda
  - Runs actual Linux containers (slower cold start than Workers)

Cold Start Benchmarks

Measured from a browser in SΓ£o Paulo, Tokyo, and Berlin accessing functions deployed on each platform:

Platform             | Cold Start | Warm P50 | Warm P99 | Notes
---------------------|------------|----------|----------|------
Cloudflare Workers   | <1ms       | 2ms      | 8ms      | Isolate model
Lambda@Edge (Node)   | 250-800ms  | 2ms      | 15ms     | Container model
Lambda@Edge (Python) | 400-1200ms | 3ms      | 20ms     | Container model
CloudFront Functions | <1ms       | 1ms      | 5ms      | JS only, very limited
AWS Lambda (us-east) | 100-400ms  | 1ms      | 10ms     | Not at edge
Vercel Edge Runtime  | <1ms       | 3ms      | 12ms     | Workers-based
Deno Deploy          | <5ms       | 4ms      | 15ms     | Deno V8 isolates

Cloudflare Workers: Complete Guide

Getting Started

# Install Wrangler CLI
npm install -g wrangler

# Create a new Worker
wrangler init my-api --type javascript
cd my-api

# Development (local)
wrangler dev

# Deploy
wrangler deploy

Worker with TypeScript (Modern Pattern)

// src/index.ts
export interface Env {
  // KV namespace bindings
  CACHE: KVNamespace
  // D1 database
  DB: D1Database
  // R2 bucket
  FILES: R2Bucket
  // Secrets
  JWT_SECRET: string
  API_KEY: string
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url)
    
    // Router
    if (url.pathname.startsWith('/api/')) {
      return handleAPI(request, env, ctx)
    }
    
    if (url.pathname.startsWith('/static/')) {
      return handleStatic(request, env)
    }
    
    return new Response('Not Found', { status: 404 })
  },
}

async function handleAPI(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
  // Auth check
  const token = request.headers.get('Authorization')?.replace('Bearer ', '')
  if (!token || !await verifyToken(token, env.JWT_SECRET)) {
    return new Response('Unauthorized', { status: 401 })
  }
  
  const url = new URL(request.url)
  const cacheKey = request.url
  
  // Check KV cache first
  const cached = await env.CACHE.get(cacheKey)
  if (cached) {
    return new Response(cached, {
      headers: { 'Content-Type': 'application/json', 'X-Cache': 'HIT' }
    })
  }
  
  // Query D1 database
  const { results } = await env.DB.prepare(
    'SELECT * FROM products WHERE active = 1 ORDER BY created_at DESC LIMIT 20'
  ).all()
  
  const responseBody = JSON.stringify(results)
  
  // Cache for 60 seconds (in background, after response)
  ctx.waitUntil(env.CACHE.put(cacheKey, responseBody, { expirationTtl: 60 }))
  
  return new Response(responseBody, {
    headers: { 'Content-Type': 'application/json', 'X-Cache': 'MISS' }
  })
}

Cloudflare Workers AI (Edge Inference)

// AI inference at the edge β€” no cold start, no GPU provisioning
export interface Env {
  AI: Ai
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const body = await request.json() as { text: string }
    
    // Text classification at edge
    const result = await env.AI.run('@cf/huggingface/distilbert-sst-2-int8', {
      text: body.text
    })
    
    return Response.json(result)
    // result: [{ label: 'POSITIVE', score: 0.9998 }]
  }
}

// Also available:
// LLM inference: @cf/meta/llama-3-8b-instruct
// Text to image: @cf/stabilityai/stable-diffusion-xl-base-1.0
// Speech to text: @cf/openai/whisper
// Embeddings: @cf/baai/bge-base-en-v1.5

Durable Objects: Stateful Edge

// Durable Objects: stateful coordination at the edge
// Great for: rate limiting, WebSocket rooms, distributed counters

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 * 1000  // 1-minute window
    const limit = 100           // 100 requests per minute

    // Reset window if expired
    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)),
          'X-RateLimit-Limit': String(limit),
          'X-RateLimit-Remaining': '0'
        }
      })
    }

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

AWS Lambda: Modern Patterns

Lambda with Node.js 22 Runtime

// handler.ts
import type { APIGatewayProxyHandlerV2, APIGatewayProxyResultV2 } from 'aws-lambda'
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb'

// Initialize outside handler β€” shared across warm invocations
const dynamo = new DynamoDBClient({
  region: process.env.AWS_REGION,
  // Use HTTP keep-alive for connection reuse
  requestHandler: {
    requestTimeout: 3000,
    socketTimeout: 3000,
  }
})

export const handler: APIGatewayProxyHandlerV2 = async (event) => {
  const { pathParameters, queryStringParameters } = event
  const userId = pathParameters?.userId
  
  if (!userId) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error: 'userId required' }),
      headers: { 'Content-Type': 'application/json' }
    }
  }
  
  try {
    const result = await dynamo.send(new GetItemCommand({
      TableName: process.env.USERS_TABLE,
      Key: { id: { S: userId } }
    }))
    
    if (!result.Item) {
      return { statusCode: 404, body: JSON.stringify({ error: 'Not found' }) }
    }
    
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 'max-age=60'
      },
      body: JSON.stringify(result.Item)
    }
  } catch (err) {
    console.error('DynamoDB error:', err)
    return { statusCode: 500, body: JSON.stringify({ error: 'Internal error' }) }
  }
}

Lambda Cold Start Optimization

// Strategies to reduce cold start latency:

// 1. Provisioned Concurrency (keeps N instances warm, eliminates cold starts)
// AWS::Lambda::Alias ProvisionedConcurrencyConfig

// 2. Lambda SnapStart (Java only, pre-initialized JVM state)

// 3. Minimize bundle size (faster container init)
// Use esbuild/rollup to tree-shake
// Bundle size matters: 1MB = ~50ms extra cold start

// 4. Move initialization outside handler
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'

const secretsClient = new SecretsManagerClient({})
let cachedSecrets: Record<string, string> | null = null

async function getSecrets() {
  if (cachedSecrets) return cachedSecrets  // Reuse across warm invocations
  
  const result = await secretsClient.send(new GetSecretValueCommand({
    SecretId: process.env.SECRET_ARN
  }))
  
  cachedSecrets = JSON.parse(result.SecretString || '{}')
  return cachedSecrets
}

export const handler = async (event: any) => {
  const secrets = await getSecrets()  // Fast on warm invocations
  // ...
}

// 5. Use ARM64 (Graviton2) β€” 34% better price/performance
// lambda.architecture = 'arm64' in CDK/Terraform

Lambda Function URLs (No API Gateway)

// Direct Lambda invocation without API Gateway overhead
// ~50ms less latency, lower cost

// Terraform:
resource "aws_lambda_function_url" "api" {
  function_name      = aws_lambda_function.api.function_name
  authorization_type = "AWS_IAM"  // or "NONE" for public

  cors {
    allow_credentials = true
    allow_origins     = ["https://app.company.com"]
    allow_methods     = ["GET", "POST", "PUT", "DELETE"]
    allow_headers     = ["Authorization", "Content-Type"]
    expose_headers    = ["X-Request-Id"]
    max_age           = 86400
  }
}

output "function_url" {
  value = aws_lambda_function_url.api.function_url
}

Decision Framework

Choose Cloudflare Workers When:

  • Sub-millisecond cold starts are required β€” auth middleware, rate limiting, A/B testing
  • Global distribution matters β€” 300+ PoPs vs Lambda's ~30 regions
  • You need edge AI inference β€” Workers AI has built-in models at the edge
  • JavaScript/TypeScript is your primary language β€” Workers runtime is mature for JS/TS/WASM
  • You're already using Cloudflare β€” tight integration with CDN, DNS, DDoS protection
  • Real-time collaboration β€” Durable Objects for WebSocket coordination
  • Cost-sensitive at high volume β€” Workers free tier: 100k requests/day; $5/mo for 10M requests

Choose AWS Lambda When:

  • Long-running tasks β€” video processing, ML training jobs (up to 15 minutes)
  • Language diversity β€” Python, Java, Go, .NET, Ruby, custom runtimes
  • Deep AWS integration β€” SQS, SNS, DynamoDB streams, S3 events, Kinesis
  • Large memory workloads β€” up to 10GB RAM; Workers max is 128MB
  • VPC access required β€” direct access to RDS, ElastiCache, private services
  • Existing AWS infrastructure β€” avoiding multi-cloud complexity
  • Compliance/data residency β€” data stays in specific AWS regions

Hybrid Architecture (Best of Both)

User β†’ Cloudflare Workers (edge logic: auth, routing, rate limiting, caching)
          ↓
     Cloudflare Origin Shield
          ↓
     AWS API Gateway + Lambda (business logic, database access, long processing)
          ↓
     RDS / DynamoDB / S3

Benefits:
- Workers: <5ms for cached responses, no origin hits
- Workers: rate limiting, auth at edge (blocks bad traffic before hitting Lambda)
- Lambda: full AWS ecosystem, VPC access, long-running tasks
- Workers smart routing: nearest Lambda region for cache misses

Cost Comparison (10M requests/month)

Cloudflare Workers:
  Plan: Workers Paid ($5/month base)
  10M requests: included in $5/month
  KV reads: $0.50/million
  Total: ~$5-10/month

AWS Lambda (128MB, 100ms avg duration, us-east-1):
  Requests: 10M Γ— $0.20/1M = $2.00
  Compute: 10M Γ— 0.1s Γ— 128MB Γ— $0.0000166667/GB-s = $2.13
  API Gateway: 10M Γ— $3.50/1M = $35.00
  Total: ~$39/month

AWS Lambda Function URL (no API Gateway):
  Requests + Compute: ~$4.13
  CloudFront: add $0.85/10GB transfer
  Total: ~$5-6/month

Lambda@Edge (same compute, at CloudFront edge):
  5x more expensive than regional Lambda
  Total: ~$20-25/month

Verdict: Workers and Lambda (without API GW) are cost-competitive.
Lambda with API Gateway is 5-8x more expensive than Workers at scale.

Conclusion

The edge computing space has matured significantly. Cloudflare Workers wins on cold start performance, global distribution, and simplicity for JavaScript workloads. AWS Lambda wins on ecosystem depth, language support, memory limits, and integration with existing AWS infrastructure.

For most modern applications, a hybrid approach makes sense: Cloudflare Workers handles the hot path (authentication, routing, caching, rate limiting) while Lambda handles business logic that needs database access or longer execution times. This combination delivers sub-5ms response times for cached content while retaining full Lambda flexibility for complex operations.

A

Alex Thompson

CEO & Cloud Architecture Expert at ZeonEdge with 15+ years building enterprise infrastructure.

Ready to Transform Your Infrastructure?

Let's discuss how we can help you achieve similar results.