Chapter 34: Serverless & FaaS

Serverless computing represents a paradigm shift where infrastructure management is abstracted away. Despite the name, servers still exist—you just don't manage them. This chapter explores Functions as a Service (FaaS), cold starts, event-driven patterns, and when serverless makes sense versus containers or VMs.

1. What is Serverless

Serverless is a cloud execution model where the provider manages all infrastructure—servers, scaling, patching, and capacity planning. You deploy code, and the platform handles everything else.

Core Characteristics

Interview Insight: Clarify that "serverless" is NOT just Lambda functions. It includes:
  • FaaS: AWS Lambda, Azure Functions, Google Cloud Functions
  • Managed Services: DynamoDB, S3, API Gateway, EventBridge
  • Containers: AWS Fargate, Google Cloud Run (serverless containers)
  • BaaS: Firebase, Auth0, Algolia (backend services)
The key is operational abstraction, not the technology itself.

Serverless vs Traditional

Aspect Traditional (VMs/Containers) Serverless
Provisioning Manual (EC2, ECS, k8s) Automatic
Scaling Configure ASG, HPA Automatic (0 to thousands)
Idle Cost Pay for running instances Zero (pay per invocation)
Cold Start None (always running) Yes (container + runtime init)
Execution Duration Unlimited 15 minutes max (Lambda)
State Persistent (local disk, memory) Ephemeral (/tmp cleared)

2. FaaS (Functions as a Service)

FaaS is the most common serverless model: deploy functions that execute in response to events. The provider manages runtime, scaling, and infrastructure.

Major FaaS Providers

Provider Service Runtimes Max Timeout Max Memory
AWS Lambda Node, Python, Java, Go, .NET, Ruby, Custom 15 minutes 10 GB
Azure Functions Node, Python, Java, .NET, PowerShell 10 minutes (Consumption), Unlimited (Premium) 1.5 GB (Consumption), 14 GB (Premium)
Google Cloud Cloud Functions Node, Python, Go, Java, .NET, Ruby, PHP 60 minutes (2nd gen) 16 GB
Cloudflare Workers JavaScript, WASM 30 seconds (free), no limit (paid) 128 MB

FaaS Execution Model

sequenceDiagram participant Client participant API Gateway participant Lambda Service participant Lambda Container participant Backend (DB/S3) Client->>API Gateway: HTTP Request API Gateway->>Lambda Service: Invoke Function alt Cold Start (no warm container) Lambda Service->>Lambda Container: Create Container Lambda Container->>Lambda Container: Load Runtime Lambda Container->>Lambda Container: Load Code Lambda Container->>Lambda Container: Initialize (100-1000ms) else Warm Start (container exists) Note over Lambda Container: Reuse existing container end Lambda Container->>Backend (DB/S3): Query Data Backend (DB/S3)-->>Lambda Container: Response Lambda Container->>Lambda Service: Return Result Lambda Service->>API Gateway: Result API Gateway->>Client: HTTP Response Note over Lambda Container: Container kept warm
for ~10-15 minutes
Key Concept: Lambda maintains a pool of execution environments (containers). When no requests arrive for ~10-15 minutes, containers are terminated. Next invocation triggers a cold start.

Lambda Anatomy

// Node.js Lambda Handler
export const handler = async (event, context) => {
    // event: trigger data (API Gateway request, S3 event, etc.)
    // context: runtime info (requestId, functionName, remainingTime)

    console.log('Event:', JSON.stringify(event));
    console.log('Request ID:', context.requestId);
    console.log('Time remaining (ms):', context.getRemainingTimeInMillis());

    // Business logic
    const result = await processData(event.body);

    // Return response
    return {
        statusCode: 200,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ result })
    };
};

3. Cold Starts

A cold start is the initialization latency when a function is invoked after being idle. This is the biggest drawback of serverless for latency-sensitive applications.

Cold Start Phases

  1. Container Creation: AWS provisions execution environment (micro-VM or container)
  2. Runtime Initialization: Load runtime (Node.js, Python, JVM, .NET CLR)
  3. Code Initialization: Download deployment package, import dependencies, run init code
  4. Handler Execution: Run your function code (this is NOT part of cold start)
graph LR A[Invocation] --> B{Warm Container
Available?} B -->|Yes| C[Execute Handler
~1-10ms] B -->|No| D[Create Container
~50-200ms] D --> E[Load Runtime
~100-500ms] E --> F[Load Code
~50-300ms] F --> G[Run Init Code
~50-1000ms] G --> C style C fill:#90EE90 style D fill:#FFB6C1 style E fill:#FFB6C1 style F fill:#FFB6C1 style G fill:#FFB6C1

Cold Start Latencies (AWS Lambda)

Runtime Cold Start (No Deps) Cold Start (With Deps) Warm Start
Node.js 100-200 ms 200-500 ms 1-10 ms
Python 150-300 ms 300-800 ms 1-10 ms
Go 100-200 ms 150-300 ms 1-5 ms
Java (JVM) 800-1500 ms 2000-4000 ms 5-20 ms
Java (SnapStart) 200-500 ms 300-800 ms 5-20 ms
.NET 500-1000 ms 1000-2000 ms 5-15 ms
Interview Question: "How would you mitigate cold starts for a latency-sensitive API?"
  1. Provisioned Concurrency: Keep N instances always warm (eliminates cold starts, but costs more)
  2. Keep-Warm Strategy: Scheduled pings (CloudWatch Events) every 5-10 minutes
  3. Smaller Packages: Reduce dependencies, use Lambda Layers, tree-shake bundles
  4. Choose Fast Runtimes: Go/Node.js over Java/.NET (or use SnapStart for Java)
  5. VPC Optimization: Avoid VPC if possible (adds ~1s cold start); use Hyperplane ENI if required
  6. AWS SnapStart (Java): Snapshot initialized function, restore from snapshot (10x faster)
  7. Consider Containers: If cold starts unacceptable, use ECS/Fargate/Cloud Run with min instances

Cold Start Triggers

Provisioned Concurrency

Keep N execution environments initialized and warm at all times. Eliminates cold starts but incurs hourly charges (similar to always-on instances).

# Configure provisioned concurrency
aws lambda put-provisioned-concurrency-config \
  --function-name my-function \
  --provisioned-concurrent-executions 10

# Cost: ~$0.015/hour per GB-hour (vs $0.0000166667/GB-second on-demand)

4. Event-Driven Architecture

Serverless functions are event-driven: they execute in response to triggers from other services. This decouples components and enables reactive, scalable architectures.

AWS Lambda Event Sources

Event Source Use Case Invocation Type Concurrency Model
API Gateway REST/HTTP APIs Synchronous 1 invocation per request
S3 Object upload/delete Asynchronous 1 invocation per object event
DynamoDB Streams Database changes (CDC) Stream (Poll) 1 invocation per shard
SQS Queue processing Stream (Poll) Batch size 1-10 msgs, concurrent pollers
Kinesis Real-time stream processing Stream (Poll) 1 invocation per shard
SNS Pub/sub messaging Asynchronous 1 invocation per message
EventBridge Event bus, scheduled tasks Asynchronous 1 invocation per event
ALB (Load Balancer) HTTP load balancing Synchronous 1 invocation per request

Event-Driven Pattern Example

graph TB A[User Upload] -->|PUT| B[S3 Bucket] B -->|S3 Event| C[Lambda: Process File] C -->|Write Metadata| D[DynamoDB] D -->|DynamoDB Stream| E[Lambda: Index Search] E -->|Index| F[OpenSearch] C -->|Publish| G[SNS Topic] G -->|Fanout| H[Lambda: Send Email] G -->|Fanout| I[Lambda: Webhook] style B fill:#FF9900 style C fill:#FF9900 style D fill:#4B92D0 style E fill:#FF9900 style F fill:#005EB8 style G fill:#FF4F8B style H fill:#FF9900 style I fill:#FF9900
Invocation Types:
  • Synchronous: Caller waits for response (API Gateway, ALB). Function errors propagate to caller.
  • Asynchronous: Lambda queues invocation, returns immediately (S3, SNS). Retries on failure (2 times by default).
  • Stream (Poll-Based): Lambda polls source (SQS, Kinesis, DynamoDB Streams). Batch processing.

Event Structure Examples

// API Gateway Event
{
    "httpMethod": "POST",
    "path": "/users",
    "headers": { "Content-Type": "application/json" },
    "body": "{\"name\":\"Alice\"}",
    "requestContext": { "requestId": "abc123" }
}

// S3 Event
{
    "Records": [{
        "eventName": "ObjectCreated:Put",
        "s3": {
            "bucket": { "name": "my-bucket" },
            "object": { "key": "uploads/file.pdf", "size": 1024000 }
        }
    }]
}

// SQS Event (Batch)
{
    "Records": [
        { "messageId": "msg1", "body": "{\"orderId\":123}" },
        { "messageId": "msg2", "body": "{\"orderId\":124}" }
    ]
}

5. Execution Model

Lambda Lifecycle

  1. Invocation: Function triggered by event or direct invocation
  2. Execution Environment: AWS creates/reuses micro-VM with runtime
  3. Handler Execution: Your code runs (subject to timeout, memory limits)
  4. Response: Return value sent to caller (synchronous) or discarded (async)
  5. Container Reuse: Environment kept warm for next invocation (~10-15 min)

Key Constraints

Constraint AWS Lambda Limit Notes
Timeout 15 minutes max Configure 1s to 900s. Use Step Functions for longer workflows.
Memory 128 MB to 10 GB CPU scales proportionally (1792 MB = 1 vCPU, 10 GB = ~6 vCPUs)
Ephemeral Storage 512 MB to 10 GB (/tmp) Cleared between invocations (but may persist across warm starts)
Deployment Package 50 MB (zipped), 250 MB (unzipped) Use Lambda Layers or container images (10 GB) for large deps
Concurrency 1000 concurrent executions (default) Soft limit, can request increase. Reserved concurrency per function.
Environment Variables 4 KB total Use Systems Manager Parameter Store / Secrets Manager for large configs
Interview Question: "Lambda gives you 10 GB of memory. How does CPU scale?"

Lambda allocates CPU proportional to memory:

  • 128 MB: ~0.07 vCPU (very slow)
  • 1792 MB: 1 full vCPU
  • 3584 MB: 2 vCPUs
  • 10 GB: ~6 vCPUs

Key insight: If your function is CPU-bound (image processing, data transformation), increasing memory speeds it up AND may reduce cost (faster execution offsets higher per-ms cost).

Stateless Execution

Lambda functions are stateless: each invocation is independent. However, containers may be reused (warm starts), so:

// Connection pooling example (global scope)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

const dynamoDb = new DynamoDBClient({}); // Created once, reused across warm invocations

export const handler = async (event) => {
    // dynamoDb client reused (no reconnection overhead)
    const result = await dynamoDb.send(new GetItemCommand({ ... }));
    return result;
};

6. Common Patterns

1. API Backend

Use Lambda + API Gateway to build serverless REST APIs. Each endpoint is a separate function (or use monolithic handler).

graph LR A[Client] -->|HTTPS| B[API Gateway] B -->|GET /users| C[Lambda: Get Users] B -->|POST /users| D[Lambda: Create User] B -->|GET /orders| E[Lambda: Get Orders] C --> F[DynamoDB] D --> F E --> F style B fill:#FF4F8B style C fill:#FF9900 style D fill:#FF9900 style E fill:#FF9900 style F fill:#4B92D0

Pros: Zero idle cost, auto-scale, pay per request

Cons: Cold starts (add provisioned concurrency for critical paths), 15-min timeout

2. Event Processing

Process events from queues, streams, or storage. Common for ETL, notifications, and async workflows.

graph LR A[Service A] -->|Publish| B[SQS Queue] B -->|Poll| C[Lambda: Process] C -->|Transform| D[Lambda: Enrich] D -->|Write| E[S3 / DynamoDB] style B fill:#FF4F8B style C fill:#FF9900 style D fill:#FF9900 style E fill:#FF9900

Example: Image upload to S3 triggers Lambda to resize, OCR text, store metadata in DynamoDB.

3. Scheduled Tasks (Cron Jobs)

Use EventBridge (CloudWatch Events) to trigger Lambda on a schedule.

# EventBridge rule: every 5 minutes
rate(5 minutes)

# Cron expression: daily at 2 AM UTC
cron(0 2 * * ? *)

Use Cases: Backups, cleanup, report generation, health checks

4. Stream Processing

Process real-time streams from Kinesis or DynamoDB Streams.

graph LR A[IoT Devices] -->|Ingest| B[Kinesis Data Stream] B -->|Shard 1| C[Lambda Instance 1] B -->|Shard 2| D[Lambda Instance 2] C -->|Aggregate| E[DynamoDB] D -->|Aggregate| E E -->|Stream| F[Lambda: Analytics] F --> G[CloudWatch Metrics] style B fill:#6CB4E8 style C fill:#FF9900 style D fill:#FF9900 style E fill:#4B92D0 style F fill:#FF9900

Note: Lambda processes stream in batches (100-10000 records). Parallelism = shard count.

5. Orchestration with Step Functions

Coordinate multiple Lambda functions into workflows. Step Functions handles retries, error handling, and state management.

graph TD A[Start] --> B[Lambda: Validate] B -->|Valid| C[Lambda: Process] B -->|Invalid| D[Lambda: Reject] C --> E{Success?} E -->|Yes| F[Lambda: Notify] E -->|No| G[Lambda: Retry] G --> C F --> H[End] D --> H style B fill:#FF9900 style C fill:#FF9900 style D fill:#FF9900 style F fill:#FF9900 style G fill:#FF9900

Use Case: Order processing (validate payment, update inventory, send confirmation, handle failures)

Lambda Limits for Long Workflows: Max 15-minute execution. For longer tasks:
  • Step Functions: Orchestrate multiple short Lambdas (workflow can run days/months)
  • ECS/Fargate: Use containers for tasks requiring hours (batch processing, ML training)
  • Batch: AWS Batch for large-scale compute jobs

7. Limitations

Limitation Impact Mitigation
Cold Starts 100-4000ms latency spikes Provisioned concurrency, keep-warm, faster runtimes (Go/Node), SnapStart
Timeout (15 min) Cannot run long-running tasks Step Functions (chain functions), ECS/Fargate for long tasks, AWS Batch
Stateless No persistent local storage Use S3, DynamoDB, EFS (mounted filesystem), ElastiCache
Vendor Lock-In Tight coupling to AWS APIs Abstract event handling, use containers (Fargate/ECS), multi-cloud frameworks (Serverless Framework)
Debugging Difficulty No SSH, limited observability CloudWatch Logs, X-Ray tracing, Lambda Insights, local testing (SAM CLI, LocalStack)
Cost at High Scale Per-request pricing expensive at 24/7 high traffic Use containers (ECS/Fargate) or VMs for sustained high throughput
Concurrency Limits 1000 concurrent executions (default) Request limit increase, reserved concurrency per function, throttling via SQS
Package Size 50 MB zipped, 250 MB unzipped Lambda Layers, container images (10 GB), dependency optimization
VPC Cold Starts +1-2 seconds for VPC ENI attachment Hyperplane ENI (newer, faster), avoid VPC if public access acceptable
Interview Question: "When would you NOT use serverless?"
  1. Latency-Critical APIs: Sub-50ms P99 required, cannot tolerate cold starts
  2. Long-Running Tasks: Video encoding (hours), ML training, batch jobs (use Batch, ECS)
  3. Sustained High Traffic: 24/7 high QPS (containers cheaper than per-request pricing)
  4. Stateful Applications: WebSocket servers, game servers, caching layers (use EC2, ECS)
  5. Complex Networking: Requires specific IP addresses, VPC peering, firewall rules
  6. Legacy Frameworks: Large monoliths difficult to refactor (use containers as intermediate step)

8. BaaS (Backend as a Service)

BaaS provides fully managed backend services: authentication, databases, storage, search, and APIs. You integrate via SDKs; provider manages infrastructure, scaling, and security.

BaaS vs FaaS

Aspect FaaS (Lambda) BaaS (Firebase, Supabase)
Control Write custom code Use pre-built services
Flexibility Any logic, any language Limited to provider's features
Setup Complexity Configure triggers, IAM, API Gateway SDK integration, minimal config
Use Case Custom business logic, integrations Common backend features (auth, CRUD)

Popular BaaS Providers

Service Features Best For
Firebase Realtime Database, Auth, Cloud Functions, Hosting, Analytics Mobile apps, realtime features (chat, collaboration)
Supabase Postgres, Auth, Storage, Realtime, Edge Functions Open-source Firebase alternative, SQL-first apps
Auth0 Authentication, SSO, MFA, User Management Enterprise auth, social login, identity management
Algolia Search API, instant results, typo tolerance Product search, site search, autocomplete
Contentful Headless CMS, content API Content-driven sites, marketing pages
Stripe Payment processing, subscriptions, invoicing E-commerce, SaaS billing

BaaS Example (Firebase)

// Frontend code (no backend required)
import { initializeApp } from 'firebase/app';
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';
import { getFirestore, collection, addDoc } from 'firestore/firestore';

const app = initializeApp({ /* config */ });
const auth = getAuth(app);
const db = getFirestore(app);

// Authenticate
await signInWithEmailAndPassword(auth, email, password);

// Write to database
await addDoc(collection(db, 'orders'), {
    userId: auth.currentUser.uid,
    items: cartItems,
    total: 99.99,
    createdAt: new Date()
});

// Realtime listener (updates UI automatically)
onSnapshot(collection(db, 'orders'), (snapshot) => {
    snapshot.forEach(doc => console.log(doc.data()));
});
When to Use BaaS:
  • Rapid Prototyping: MVP, hackathons, startups (validate idea quickly)
  • Standard Features: Auth, CRUD, search (don't reinvent the wheel)
  • Small Teams: Limited backend expertise or resources
When to Avoid BaaS:
  • Complex Logic: Custom workflows, multi-step transactions
  • Data Ownership: Regulatory requirements, data residency
  • Vendor Lock-In Risk: Strategic apps, long-term (10+ years)

9. Serverless vs Containers

Decision Matrix

Factor Choose Serverless (Lambda) Choose Containers (ECS/Fargate/k8s)
Traffic Pattern Sporadic, unpredictable, bursty (0 to 1000 QPS) Sustained high traffic (24/7, stable QPS)
Latency Requirement Can tolerate 100-500ms cold starts (or use provisioned concurrency) P99 latency under 50ms, no cold starts acceptable
Execution Duration Under 15 minutes Hours or indefinite (batch jobs, streaming)
State Stateless, event-driven Stateful (WebSockets, sessions, caches)
Cost Profile Low traffic (idle cost = 0) High sustained traffic (per-request pricing expensive)
Complexity Simple functions, microservices Monoliths, complex apps, legacy systems
Deployment Frequent small updates Infrequent deployments, long-running processes
Vendor Lock-In Acceptable (or use Serverless Framework for portability) Must avoid (use containers for multi-cloud portability)
Interview Scenario: "You're building a video transcoding service. Serverless or containers?"

Analysis:

  • Execution Duration: Video transcoding can take hours (Lambda max = 15 min). Containers win.
  • Cost: Sporadic traffic (users upload videos irregularly). Serverless could scale to zero. Serverless wins.
  • State: Stateless (read video from S3, transcode, write back). Serverless OK.

Solution: Use AWS Batch or ECS Fargate (serverless containers):

  • ECS Fargate: Run containers without managing EC2 (scales to zero with Application Auto Scaling)
  • AWS Batch: Optimized for batch jobs (queues, priorities, spot instances)
  • Trigger: Lambda receives S3 upload event, submits Fargate/Batch job for transcoding

Hybrid Approach

Many systems use both:

10. Cost Analysis

Lambda Pricing Model (AWS, us-east-1)

Cost Example: REST API

Scenario: 1 million requests/month, 1 GB memory, 200ms avg execution

# Lambda (on-demand)
Requests: 1M * $0.20 / 1M = $0.20
Compute: 1M * 0.2s * 1GB * $0.0000166667 = $3.33
Total: $3.53/month

# EC2 t3.small (always-on)
Instance cost: $0.0208/hour * 730 hours = $15.18/month

# Break-even: ~4M requests/month (at 200ms, 1GB)
Cost Insight: Lambda is cheaper for:
  • Low/Sporadic Traffic: Under 5M requests/month (scales to zero)
  • Short Execution: Under 1 second (minimize compute cost)
Containers/VMs are cheaper for:
  • High Sustained Traffic: 24/7, millions of requests/day
  • Long Execution: Minutes per request (compute cost accumulates)

Cost Comparison Table

Traffic Pattern Lambda Cost (1GB, 200ms) EC2 t3.small Cost Winner
100K requests/month $0.35 $15.18 Lambda
1M requests/month $3.53 $15.18 Lambda
10M requests/month $35.30 $15.18 EC2
100M requests/month $353.00 $15.18 (or $50 for m5.large) EC2

Provisioned Concurrency Cost

# Keep 10 instances warm (1 GB each, 24/7)
Provisioned cost: 10 * 1GB * 730 hours * $0.015 = $109.50/month

# On-demand compute still charged for execution time
Execution cost: (same as on-demand)

# Total: $109.50 + execution cost

When to use: Latency-critical APIs with unpredictable traffic (cold starts unacceptable, but don't want always-on VMs). Often cheaper than EC2 if traffic is still bursty.

Cost Optimization Strategies

  1. Right-Size Memory: Test different memory settings (CPU scales with memory). Higher memory may be cheaper if faster execution offsets cost.
  2. Batch Processing: Process SQS/Kinesis in batches (10-100 records) to amortize invocation cost.
  3. Connection Pooling: Reuse DB connections across warm invocations (avoid reconnection overhead).
  4. Reduce Package Size: Smaller deployments = faster cold starts = lower init cost.
  5. Use Lambda Layers: Share dependencies across functions (reduce redundant uploads, faster deploys).
  6. Monitor with Cost Explorer: Track Lambda cost by function, identify expensive outliers.
  7. Shift to Containers: At high sustained traffic (10M+ req/month), ECS/Fargate often cheaper.
Interview Question: "Your Lambda bill jumped from $100 to $10,000. How do you investigate?"
  1. Check CloudWatch Metrics: Invocation count, duration, errors (unexpected traffic spike? infinite loop?)
  2. Review Cost Explorer: Break down by function, region, time (which function is expensive?)
  3. Check Logs: CloudWatch Logs for errors, retries (async invocations retry 2x on failure)
  4. Look for Infinite Loops: Lambda invoking itself via S3 events (misconfigured triggers)
  5. Memory Configuration: Over-provisioned memory (1GB when 128MB sufficient)
  6. DLQ/SQS Backlog: Failed messages retrying infinitely (configure DLQ, set max retries)

Chapter Summary

Interview Tips