Serverless Computing Guide 2026 — AWS Lambda, Cloudflare Workers, and Edge Functions
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-offlineCloudflare 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
| Platform | Cold Start | Warm State | Cost Model |
|---|---|---|---|
| AWS Lambda (Node.js) | 100-500ms | ~1ms | Per 100ms + per request |
| Cloudflare Workers | ~0ms | ~0ms | Per request |
| Vercel Edge | ~0ms | ~0ms | Per request |
| AWS Lambda (arm64) | 80-400ms | ~1ms | 20% 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: arm64When 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.13Common 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 = falsein 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
Related reading
AWS Lambda — Serverless Functions Complete Guide5 min readKubernetes on AWS EKS — Complete Setup Guide 20266 min readTerraform Complete Guide — Infrastructure as Code for Cloud Engineers5 min readAWS EC2 Complete Guide — Launch, Scale, and Secure Virtual Machines5 min readAWS S3 — Object Storage, Static Hosting, and CDN Guide4 min readAWS RDS — Managed Relational Database Complete Guide5 min read