Serverless Computing Guide 2026 — AWS Lambda, Cloudflare Workers, and Edge Functions

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Introduction

Why This Matters

Serverless means no servers to manage. Code runs in response to events, scales to zero when idle, and scales to millions when needed. You pay only for actual compute time. In 2026, serverless is the default choice for webhook handlers, background jobs, scheduled tasks, and bursty workloads. This guide covers AWS Lambda, Cloudflare Workers, and when each platform wins.

AWS Lambda: Event-Driven Functions

// handler.ts — Fully typed Lambda HTTP handler
import type {
  Handler,
  APIGatewayProxyEventV2,
  APIGatewayProxyResultV2,
  SQSHandler,
  S3Handler,
  ScheduledHandler,
} from 'aws-lambda'
 
const headers = {
  'Content-Type': 'application/json',
  'Access-Control-Allow-Origin': '*',
}
 
// HTTP handler
export const handler: Handler<APIGatewayProxyEventV2, APIGatewayProxyResultV2> = async (event) => {
  try {
    const method = event.requestContext.http.method
    const path = event.requestContext.http.path
 
    if (method === 'GET' && path === '/users') {
      const users = await getUsers()
      return { statusCode: 200, headers, body: JSON.stringify(users) }
    }
 
    if (method === 'POST' && path === '/users') {
      const body = JSON.parse(event.body || '{}')
      const user = await createUser(body)
      return { statusCode: 201, headers, body: JSON.stringify(user) }
    }
 
    return { statusCode: 404, headers, body: JSON.stringify({ error: 'Not found' }) }
  } catch (error) {
    console.error('Handler error:', error)
    return { statusCode: 500, headers, body: JSON.stringify({ error: 'Internal error' }) }
  }
}
 
// SQS queue processor
export const sqsHandler: SQSHandler = async (event) => {
  for (const record of event.Records) {
    const message = JSON.parse(record.body)
    await processMessage(message)
  }
}
 
// S3 event trigger
export const s3Handler: S3Handler = async (event) => {
  for (const record of event.Records) {
    const bucket = record.s3.bucket.name
    const key = decodeURIComponent(record.s3.object.key)
    await processUpload(bucket, key)
  }
}
 
// EventBridge scheduled trigger
export const scheduledHandler: ScheduledHandler = async () => {
  await runDailyCleanup()
}

Serverless Framework Configuration

# serverless.yml
service: my-api
frameworkVersion: '3'
 
provider:
  name: aws
  runtime: nodejs20.x
  region: us-east-1
  memorySize: 512
  timeout: 10
  architecture: arm64   # 20% cheaper than x86
 
  environment:
    DATABASE_URL: ${ssm:/myapp/database-url}
    REDIS_URL: ${ssm:/myapp/redis-url}
 
  iam:
    role:
      statements:
        - Effect: Allow
          Action: [s3:GetObject, s3:PutObject]
          Resource: arn:aws:s3:::myapp-assets/*
        - Effect: Allow
          Action: [sqs:ReceiveMessage, sqs:DeleteMessage]
          Resource: !GetAtt ProcessingQueue.Arn
 
functions:
  api:
    handler: dist/handler.handler
    events:
      - httpApi:
          path: /{proxy+}
          method: ANY
    reservedConcurrency: 100
 
  processQueue:
    handler: dist/queue.sqsHandler
    events:
      - sqs:
          arn: !GetAtt ProcessingQueue.Arn
          batchSize: 10
 
  dailyReport:
    handler: dist/cron.scheduledHandler
    events:
      - schedule: cron(0 9 * * ? *)
 
plugins:
  - serverless-esbuild
  - serverless-offline

Cloudflare Workers: Zero Cold-Start Edge Computing

// worker.ts — Runs in 100+ edge locations with zero cold starts
interface Env {
  KV_STORE: KVNamespace
  API_KEY: string
  DB: D1Database
}
 
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url)
    const country = request.cf?.country
    const city = request.cf?.city
 
    if (url.pathname === '/api/geo') {
      return Response.json({ country, city, timezone: request.cf?.timezone })
    }
 
    if (url.pathname.startsWith('/api/posts')) {
      // Edge-side caching
      const cache = caches.default
      const cacheKey = new Request(url.toString())
      const cached = await cache.match(cacheKey)
      if (cached) return cached
 
      const data = await fetchFromOrigin(url.pathname, env)
      const response = Response.json(data, {
        headers: { 'Cache-Control': 'public, max-age=300' },
      })
 
      ctx.waitUntil(cache.put(cacheKey, response.clone()))
      return response
    }
 
    // KV storage (globally replicated key-value)
    if (url.pathname === '/api/config') {
      const config = await env.KV_STORE.get('site-config', 'json')
      return Response.json(config)
    }
 
    return new Response('Not found', { status: 404 })
  },
 
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
    ctx.waitUntil(syncData(env))
  },
}
# wrangler.toml
name = "my-worker"
main = "worker.ts"
compatibility_date = "2026-03-19"
 
[[kv_namespaces]]
binding = "KV_STORE"
id = "your-kv-namespace-id"
 
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "your-database-id"

Cold Starts: The Trade-off

PlatformCold StartWarm StateCost Model
AWS Lambda (Node.js)100-500ms~1msPer 100ms + per request
Cloudflare Workers~0ms~0msPer request
Vercel Edge~0ms~0msPer request
AWS Lambda (arm64)80-400ms~1ms20% cheaper than x86

Minimize Lambda cold starts:

# Provisioned Concurrency — keep N instances always warm
functions:
  api:
    provisionedConcurrency: 5   # Always warm, costs more
 
# Or use arm64 for faster startup
provider:
  architecture: arm64

When to Use Serverless

Good fits:

  • Webhook receivers and event processors
  • Background jobs and queue consumers
  • Scheduled tasks (cron jobs)
  • Bursty or unpredictable traffic patterns
  • Low-traffic APIs with cost sensitivity

Avoid serverless for:

  • Long-running tasks over 15 minutes (Lambda hard limit)
  • WebSocket servers requiring persistent connections
  • Constant high traffic above 50M requests/month (EC2 cheaper)
  • Applications that require large in-memory state

Lambda Cost Calculator

# lambda-cost.py
def calculate_cost(
    monthly_requests: int,
    avg_duration_ms: float,
    memory_mb: int,
    arch: str = 'arm64'
) -> dict:
    # arm64: $0.0000133334 per GB-second
    # x86:   $0.0000166667 per GB-second
    gb_sec_price = 0.0000133334 if arch == 'arm64' else 0.0000166667
    request_price = 0.20 / 1_000_000
 
    FREE_REQUESTS   = 1_000_000
    FREE_GB_SECONDS = 400_000
 
    billable_req = max(0, monthly_requests - FREE_REQUESTS)
    gb_seconds   = (memory_mb / 1024) * (avg_duration_ms / 1000) * monthly_requests
    billable_gb  = max(0, gb_seconds - FREE_GB_SECONDS)
 
    total = billable_req * request_price + billable_gb * gb_sec_price
    return {'monthly_cost': f'${total:.2f}', 'vs_t3_small': f'$15/mo EC2' }
 
# 5M requests, 200ms, 512MB, arm64
print(calculate_cost(5_000_000, 200, 512))
# monthly_cost: $2.13

Common Mistakes

  • No connection pooling — Lambda creates a new database connection on every cold start; use connection proxy like RDS Proxy
  • Large deployment packages — every MB of ZIP size increases cold start time; use tree-shaking and serverless-esbuild
  • Synchronous waits in SQS handlers — process messages in parallel with Promise.all() to maximize batch throughput
  • No dead-letter queue — without a DLQ, failed SQS messages retry forever and block the queue
  • Ignoring Provisioned Concurrency costs — it charges even when no requests are coming; only use it for critical paths

Best Practices

  • Use arm64 architecture for all Lambda functions — 20% cheaper with same or better performance
  • Set appropriate memory size — Lambda CPU scales proportionally to memory; often 512MB runs faster than 128MB due to more CPU
  • Use context.callbackWaitsForEmptyEventLoop = false in Node.js to prevent Lambda from waiting for async callbacks
  • Store large Lambda layers in a shared layer ARN to avoid exceeding the 250MB deployment limit per function
  • Use Lambda Power Tuning (open-source) to find the optimal memory setting that minimizes cost for your workload

Key Takeaways

  • AWS Lambda runs code in response to events (HTTP, SQS, S3, EventBridge) and charges per 100ms of execution time
  • Cloudflare Workers run at the edge with zero cold starts and a V8 isolate model that is fundamentally different from Node.js containers
  • arm64 Lambda is 20% cheaper than x86 with comparable or faster performance — use it for all new functions
  • Provisioned Concurrency eliminates cold starts at the cost of per-hour charges even when idle
  • Lambda is cost-effective up to roughly 50M requests/month; above that, a reserved EC2 instance is usually cheaper
  • SQS + Lambda is the canonical pattern for background job processing — automatically scales to batch size and handles retries
  • The 15-minute execution limit makes Lambda unsuitable for long-running data processing jobs; use ECS Fargate instead
  • Cloudflare Workers D1 (SQLite at the edge) and KV (global key-value) enable stateful edge applications without origin roundtrips

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading