Home › Cloud services › Compute
guide low

Serverless functions

Problem Teams with spiky or low-traffic workloads (webhooks, cron jobs, event handlers) want to avoid running idle servers. This guide answers when functions-on-demand fit and when they cost more or fall short.

Lambda, Cloud Run functions, Azure Functions and Cloudflare Workers run code per request and bill per millisecond; ideal for spiky or glue workloads, costly at steady high load.

lambdacloudflare-workersazure-functionscloud-runfaascold-start
Overview

Serverless functions hand the provider a handler; it starts sandboxes on demand, scales them to thousands, and bills per request plus per GB-second (or CPU-ms). You never see a server and pay nothing when idle. The trade: cold starts, execution limits, per-unit prices that exceed a busy VM, and connection-hungry databases behind thousands of short-lived instances.

Key facts

Use when

  • Webhooks, cron jobs, event handlers (S3 upload, queue message), low-traffic APIs, edge logic (auth, redirects, A/B).

Avoid when

  • Steady load above order of 10-50 requests/s per function, long-lived connections (WebSockets, game state), or GPU work.

Deep dive

Platform comparison

PlatformIsolationCold startBillingBest at
AWS LambdaFirecracker microVM100 ms-1 sRequests + GB-sGlue inside AWS, event sources (SQS, S3, DynamoDB streams)
Cloud Run (services/functions)gVisor/microVM container0.5-2 svCPU-s + GB-s, concurrency per instanceAny container, many requests per instance
Azure Functions (Flex Consumption)Container0.5-2 sExecutions + GB-sAzure-native events
Cloudflare WorkersV8 isolate< 5 msRequests + CPU-msEdge APIs, auth, HTML rewriting, global low latency
Vercel/Netlify functionsOn Lambda or own edgevariesBundled in planFrontend 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

SituationChoiceWhy
Event glue inside AWSLambdaNative triggers, IAM, zero ops
Global API needing < 50 ms TTFB everywhereCloudflare Workers + KV/D1/Durable Objects or a regional DB with cacheRuns in 300+ PoPs, no cold starts
Containerized HTTP service, spikyCloud Run / Container AppsConcurrency per instance cuts cost vs one-request-per-sandbox
Steady > ~20 req/s per functionContainers or VMsPer-ms billing exceeds reserved capacity
Long jobs (> 15 min), big memory, GPUBatch on VMs/containers (GPU cloud)Hard platform limits
Stateful real-time (rooms, game sessions)Durable Objects (small) or dedicated serversFunctions 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

namelangrepoinstallverdictlicencehealthas_ofnotes
cargo-lambdaRustcargo-lambda/cargo-lambdabrew install cargo-lambda/tap/cargo-lambda————build and deploy Rust Lambda functions
lambda_runtimeRustaws/aws-lambda-rust-runtimecargo add lambda_runtime————official Rust runtime, used in Code above
workers-rsRustcloudflare/workers-rscargo add worker————Rust on Cloudflare Workers (WASM)
SpinRustspinframework/spinbrew tap spinframework/tap && brew install spinframework/tap/spin————WebAssembly functions, runs anywhere
WranglerTypeScriptcloudflare/workers-sdknpm 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

Alternatives and related