Deep dive
Platform comparison
| Platform | Isolation | Cold start | Billing | Best at |
|---|---|---|---|---|
| AWS Lambda | Firecracker microVM | 100 ms-1 s | Requests + GB-s | Glue inside AWS, event sources (SQS, S3, DynamoDB streams) |
| Cloud Run (services/functions) | gVisor/microVM container | 0.5-2 s | vCPU-s + GB-s, concurrency per instance | Any container, many requests per instance |
| Azure Functions (Flex Consumption) | Container | 0.5-2 s | Executions + GB-s | Azure-native events |
| Cloudflare Workers | V8 isolate | < 5 ms | Requests + CPU-ms | Edge APIs, auth, HTML rewriting, global low latency |
| Vercel/Netlify functions | On Lambda or own edge | varies | Bundled in plan | Frontend teams |
When the VM wins
Lambda at 1 GB, 50 ms per request, steady 100 req/s:
- 100 × 86,400 × 30 = 259M requests/month → $52 requests + 259M × 0.05 GB-s × $0.0000167 ≈ $216 → order of $270/month.
- The same load fits on one 2-vCPU VM (order of $15-70/month). Break-even is typically 5-30 req/s sustained, depending on duration and memory.
Decision table
| Situation | Choice | Why |
|---|---|---|
| Event glue inside AWS | Lambda | Native triggers, IAM, zero ops |
| Global API needing < 50 ms TTFB everywhere | Cloudflare Workers + KV/D1/Durable Objects or a regional DB with cache | Runs in 300+ PoPs, no cold starts |
| Containerized HTTP service, spiky | Cloud Run / Container Apps | Concurrency per instance cuts cost vs one-request-per-sandbox |
| Steady > ~20 req/s per function | Containers or VMs | Per-ms billing exceeds reserved capacity |
| Long jobs (> 15 min), big memory, GPU | Batch on VMs/containers (GPU cloud) | Hard platform limits |
| Stateful real-time (rooms, game sessions) | Durable Objects (small) or dedicated servers | Functions are stateless and short-lived |
Code
Cloudflare Worker with caching and a token check (TypeScript):
export interface Env { API_TOKEN: string; ORIGIN: string }
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(req.url);
if (url.pathname.startsWith("/admin")) {
const auth = req.headers.get("authorization") ?? "";
if (auth !== `Bearer ${env.API_TOKEN}`) return new Response("forbidden", { status: 403 });
}
if (req.method !== "GET") return fetch(new Request(env.ORIGIN + url.pathname + url.search, req));
const cache = caches.default;
const key = new Request(url.toString(), { method: "GET" });
let res = await cache.match(key);
if (!res) {
res = await fetch(env.ORIGIN + url.pathname + url.search, { cf: { cacheTtl: 60 } });
res = new Response(res.body, res);
res.headers.set("cache-control", "public, max-age=60");
ctx.waitUntil(cache.put(key, res.clone())); // store without delaying the response
}
return res;
},
};
Lambda handler in Rust: initialize clients once, outside the handler.
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use serde_json::{json, Value};
#[tokio::main]
async fn main() -> Result<(), Error> {
let http = reqwest::Client::new(); // reused across warm invocations
run(service_fn(|ev: LambdaEvent<Value>| {
let http = http.clone();
async move {
let id = ev.payload["id"].as_str().unwrap_or("none").to_owned();
let _ = http.get("https://example.com/health").send().await;
Ok::<Value, Error>(json!({ "ok": true, "id": id }))
}
})).await
}
Efficiency
- Cost: Lambda bills per request plus duration times memory; CPU scales with memory and one vCPU arrives at 1,769 MB (Lambda quotas doc, as of 2026-10). Workers bill $0.30 per additional million requests and $0.02 per additional million CPU ms, with no duration charge, so waiting on I/O is free (Workers pricing and limits, as of 2026-10). Break-even against a VM is the sustained rate computed above. Bound: cold start and per-unit price at steady load.
- Levers:
- SnapStart — startup "as low as sub-second" (Lambda SnapStart docs) — supported runtimes with slow init
- create clients once outside the handler — warm invocations reuse them (Code above) — all Lambda handlers
- set memory by measured duration — more memory means more CPU, which can shorten the run (Lambda quotas doc) — CPU-bound handlers
- CPU-time billing on Workers — I/O wait costs nothing (Workers limits docs) — fetch-heavy edge APIs
- Arm — order of 20 % cheaper per GB-s (Key facts) — code that rebuilds for arm64
- Naive → standard → state of the art:
- new client and DB connection per invocation, slow runtime init · seconds of cold start, connection storms
- clients reused, pooler, Arm, tuned memory · warm invocation in ms (order of)
- SnapStart or V8 isolates (Workers), Cloud Run concurrency or a VM for steady load · sub-second to under 5 ms cold start (order of)
Implementation notes
- Put database access behind a pooler (RDS Proxy, PgBouncer, Hyperdrive on Cloudflare) or use an HTTP-native DB; 1000 concurrent functions = 1000 connections.
- Make handlers idempotent: event sources deliver at least once and retry on error.
- Use Arm (Graviton) for Lambda: order of 20 % cheaper per GB-s.
- Provisioned concurrency / min instances remove cold starts but bill 24/7: you are back to paying for servers.
- Send failed events to a dead-letter queue and alarm on its depth.
Pitfalls
- Recursive triggers (function writes to the bucket that triggers it) run up bills in minutes; AWS now detects some loops, not all.
- Wall-clock billing on Lambda means waiting on a slow API costs money; Workers bill CPU time only.
- Logging every request to CloudWatch at $0.50/GB ingested can exceed the compute bill.
- Payload limits (Lambda 6 MB sync), timeouts at API Gateway (29 s by default) shorter than the function's.
- VPC-attached functions need NAT for internet access, adding NAT gateway charges.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| cargo-lambda | Rust | cargo-lambda/cargo-lambda | brew install cargo-lambda/tap/cargo-lambda | — | — | — | — | build and deploy Rust Lambda functions |
| lambda_runtime | Rust | aws/aws-lambda-rust-runtime | cargo add lambda_runtime | — | — | — | — | official Rust runtime, used in Code above |
| workers-rs | Rust | cloudflare/workers-rs | cargo add worker | — | — | — | — | Rust on Cloudflare Workers (WASM) |
| Spin | Rust | spinframework/spin | brew tap spinframework/tap && brew install spinframework/tap/spin | — | — | — | — | WebAssembly functions, runs anywhere |
| Wrangler | TypeScript | cloudflare/workers-sdk | npm i -D wrangler@latest | — | — | — | — | develop and deploy Workers |
Sources
- Eric Jonas et al., Cloud Programming Simplified: A Berkeley View on Serverless Computing, UC Berkeley, 2019. paper
- Alexandru Agache et al., Firecracker: Lightweight Virtualization for Serverless Applications, NSDI 2020. paper
- AWS, AWS Lambda Pricing pricing and Lambda quotas docs; Cloudflare, Workers pricing pricing and Workers limits limits