serverless-architecture

Serverless is a billing-and-scaling model, not a badge of modernity.

SKILL.md

GitHub

Serverless Architecture (Verbose)

Core Patterns

When Serverless Wins and When It Loses

Serverless is a billing-and-scaling model, not a badge of modernity. The decision turns almost entirely on the shape of your load.

Load shape Serverless (FaaS) Always-on container / VM
Spiky, unpredictable Excellent — scales instantly, idle costs ~nothing Wasteful — provisioned for a peak that is rare
Low or zero baseline Excellent — scale-to-zero Wasteful — paying to sit idle
Event-driven / glue Excellent — natural fit Awkward — a service babysitting a queue
Steady, high throughput Poor — per-invocation cost adds up, cold starts hurt Excellent — near-capacity utilization is cheap
Latency-critical, tight p99 Risky — cold starts add tail latency Predictable — warm process always ready
Long-running jobs Poor — invocation timeout Fine — no artificial ceiling

The classic mistake is porting a steady, high-QPS API to functions to "be serverless" and being surprised by both the bill and the tail latency. The inverse mistake is running a cron job or a webhook receiver on an always-on fleet, paying 24/7 for seconds of work per day.

A useful crossover intuition: below some steady request rate, per-invocation billing is cheaper; above it, reserved always-on compute wins. You do not need the exact number — you need to know which side of it your workload sits on, and that it moves as traffic grows.

Statelessness and the Execution Lifecycle

A function instance is created, may serve one or many invocations, is frozen between them, and is eventually destroyed — all outside your control. Two consequences drive the design:

  • No durable local state. Anything written to memory or local disk may vanish before the next call and is never shared across concurrent instances. Persist state in a managed database or cache (managed-database-selection, distributed-caching-design).
  • Initialization is reused when warm. Code outside the handler runs once per instance, not once per request. Put expensive one-time setup — SDK clients, DB connection pools, config loads — there, so warm invocations skip it.
# module scope: runs at cold start, reused across warm invocations
db = connect_pool()          # do this ONCE per instance
client = StorageClient()

def handler(event, context):
    # per-request work only; keep this path lean
    return process(event, db, client)

Every platform imposes a per-invocation timeout you must design around. Work that cannot finish inside it must be decomposed into steps or handed to a batch/queue system — never assume you can raise the ceiling far enough.

Cold Starts

A cold start is the time to provision a runtime, load your code, and run initialization before the first request is served. It is paid whenever the platform adds an instance — the first request after idle, and every request that scales the fleet up.

Drivers and mitigations:

  • Package size / dependency weight → trim dependencies, avoid pulling entire SDKs when a submodule will do.
  • Heavy init → move it to module scope so it amortizes across warm calls; lazy-load anything not needed on every path.
  • Runtime choice → interpreted/lightweight runtimes generally start faster than heavy JVM-style ones.
  • VPC / private-network attachment → historically adds startup cost; keep functions out of a VPC unless they need private resources.
  • Latency-critical paths → use provisioned/warmed concurrency to keep instances hot, accepting that it reintroduces an always-on cost.

Load-test the scale-from-zero and scale-up paths explicitly (see load-testing). Cold starts are invisible in steady-state testing and then surface as p99 spikes in production.

Event-Driven Composition

Serverless systems are wired together by events, not by services calling each other directly:

S3 object created ─▶ Lambda (thumbnail)  ─▶ writes derived object
API Gateway  ─▶ Lambda (handler) ─▶ SQS ─▶ Lambda (worker) ─▶ DynamoDB
EventBridge schedule ─▶ Lambda (nightly rollup)

Keep each function single-purpose and let a queue or pub/sub topic sit between producers and consumers. That decouples them, absorbs bursts, and gives you a natural retry and dead-letter boundary. Fan-out (one event, many independent consumers) is done with a topic, not by one function invoking several others.

Orchestrating Multi-Step Workflows

Chaining functions by direct synchronous invocation is an anti-pattern: the caller is billed while it blocks waiting for the callee, and their timeouts become coupled — the outer function can time out mid-way, leaving partial work.

Use a managed orchestrator for anything with steps, branching, or retries:

  • AWS Step Functions — a JSON/ASL state machine with built-in retry, catch, parallel, and wait states.
  • Google Cloud Workflows / Azure Durable Functions — equivalents.

The orchestrator owns the state and progress; each function stays a small, stateless step. For high fan-out or buffering, prefer an asynchronous queue between steps over a synchronous chain.

Idempotency and Failure Handling

Event sources and queues generally guarantee at-least-once delivery, so any handler can run more than once on the same message — after a retry, a redelivery, or a duplicate publish. Handlers must be idempotent: processing the same event twice must not double-charge, double-send, or corrupt state (see idempotency-patterns for keys and dedup tables).

Wire a dead-letter queue on every asynchronous source so that a message which keeps failing is parked for inspection instead of retried forever or dropped silently. Emit structured logs and traces per invocation — distributed tracing across function boundaries is how you reconstruct an event's path.

Common Anti-Patterns

❌ Porting a steady high-QPS service to FaaS to "be serverless". ✅ Match the model to the load shape — always-on compute for steady throughput, serverless for spiky or event-driven work.

❌ Storing state in a function's local memory or disk and expecting it next time. ✅ Externalize all state to a managed store; treat every invocation as fresh.

❌ One function synchronously invoking the next, several deep. ✅ Use a workflow orchestrator, or decouple steps with a queue or topic.

❌ Non-idempotent handlers on an at-least-once source. ✅ Make every handler idempotent and add a dead-letter queue for poison messages.

❌ Ignoring cold starts until production. ✅ Trim packages, keep init out of the hot path, load-test scale-from-zero, and use provisioned concurrency where tail latency matters.

❌ A long-running job fighting the invocation timeout. ✅ Decompose it into steps under an orchestrator, or move it to a batch service.

❌ Putting a function in a VPC by reflex. ✅ Attach to private networking only when it needs a private resource; accept the startup cost knowingly.

Serverless Architecture Checklist

  • Load shape confirmed to favour serverless (spiky / low-baseline / event-driven)
  • All durable state externalized to a managed store
  • Expensive initialization moved to module scope, reused across warm calls
  • Work fits inside the invocation timeout, or is decomposed
  • Cold-start budget understood; scale-from-zero load-tested
  • Provisioned concurrency used only where tail latency demands it
  • Functions single-purpose, composed via events and queues/topics
  • Multi-step flows run under an orchestrator, not synchronous chains
  • Handlers idempotent against at-least-once delivery
  • Dead-letter queues on every asynchronous source
  • Structured logs and distributed tracing across function boundaries
  • Cost modeled against an always-on alternative at expected steady load