Week 18: Serverless on AWS — Lambda, API Gateway & Fargate

Everything built since Week 11 assumes a cluster of nodes you provision, patch and pay for continuously, whether or not anything is actually running on them. Not every workload needs that. This week covers the other end of the compute spectrum: Lambda functions that run only in response to an event and bill in milliseconds, API Gateway for exposing them over HTTP, and Fargate for running the same containers from earlier weeks without managing EC2 instances or a Kubernetes control plane at all.

Module 15 of 22 Week 18 of 26 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Deploy a Lambda function and reason about cold starts and event sources
  • Expose a Lambda over HTTP through API Gateway with request mapping
  • Run an existing container on Fargate and decide when it beats Lambda or Kubernetes

1. Lambda Functions, Cold Starts & Event Sources

A Lambda function is a single handler AWS runs in response to an event and stops — you're billed only for the milliseconds it actually executes, and nothing runs (or costs anything) between invocations. That's the opposite trade-off from the always-on EC2/Kubernetes model of Weeks 8–17: no idle cost, but also no persistent process to hold a warm database connection pool or in-memory cache between requests.

handler.py — a minimal Lambda handler
import json

def handler(event, context):
    name = event.get("queryStringParameters", {}).get("name", "world")
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"message": f"hello, {name}"})
    }
terminal — packaging and deploying
zip function.zip handler.py

aws lambda create-function \
  --function-name hello-api \
  --runtime python3.12 \
  --role arn:aws:iam::123456789012:role/lambda-basic-execution \
  --handler handler.handler \
  --zip-file fileb://function.zip \
  --timeout 10 \
  --memory-size 256

A cold start happens when Lambda has to initialize a fresh execution environment — download your code, start the runtime, run any top-level initialization — before it can handle the first request; a warm invocation reuses an already-initialized environment and skips all of that. Cold starts are the single biggest latency variable in Lambda: typically tens to a few hundred milliseconds for a small interpreted-language function, but they can reach seconds for a large function with a heavy runtime and lots of top-level imports.

A function can be triggered by more than an HTTP request — an S3 upload, a message landing in an SQS queue, a scheduled EventBridge rule, or a change stream from DynamoDB are all common event sources, each delivering a differently-shaped event object to the same handler signature.

Never open a database connection inside the handler body

Opening a fresh connection on every invocation adds real latency and can exhaust a database's connection limit under concurrent load. Initialize the connection at module scope, outside the handler function — on a warm invocation, that code doesn't re-run, so the connection is reused across requests, and pairing it with RDS Proxy handles the connection pooling problem at scale.

2. API Gateway Routes & Request Mapping

A Lambda function has no URL of its own — API Gateway is what turns it into an HTTP API, routing incoming requests to the right function and translating its response back into a proper HTTP response. The modern, cheaper option for a straightforward REST-style API is an HTTP API (as opposed to the older, more configurable REST API type).

terminal — wiring a Lambda behind an HTTP API
aws apigatewayv2 create-api \
  --name hello-api \
  --protocol-type HTTP \
  --target arn:aws:lambda:us-east-1:123456789012:function:hello-api

# grant API Gateway permission to invoke the function
aws lambda add-permission \
  --function-name hello-api \
  --statement-id apigw-invoke \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com

With Lambda proxy integration (the default and recommended mode), API Gateway passes the entire raw request — method, path, headers, query string, body — into the event object exactly as seen in Section 1, and expects the function's return value to fully specify the HTTP response, including statusCode and headers. The alternative, non-proxy integration lets API Gateway itself transform the request and response shape with velocity-template mapping — more configuration, rarely worth it for a new API, but worth recognizing if you inherit one.

Set a throttle limit before this goes anywhere near production

Without a configured rate limit, a runaway client or a bug in an upstream caller can drive unlimited concurrent Lambda invocations — and unlimited cost — against your account. API Gateway's per-route throttling and Lambda's own reserved/provisioned concurrency settings are the equivalent of the resource limits from Week 12's Deployments: a deliberate ceiling, not an assumption that traffic will stay reasonable.

3. Fargate: Containers Without Managing Nodes

Lambda is a poor fit for a long-running process, a large container image, or a workload that needs more than 15 minutes to complete (Lambda's hard execution limit). Fargate covers that gap: it's a launch type for ECS (or EKS) that runs the exact same Docker images from Week 3–5 without you provisioning or patching any EC2 instance — you specify CPU and memory for the task, and AWS runs the container on infrastructure it manages entirely.

task-definition.json — a Fargate task for the app from Week 5
{
  "family": "web-app",
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "512",
  "memory": "1024",
  "containerDefinitions": [
    {
      "name": "web-app",
      "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/web-app:v1.4.0",
      "portMappings": [{ "containerPort": 8000, "protocol": "tcp" }],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/web-app",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "web-app"
        }
      }
    }
  ]
}
terminal — running it as an ECS service behind a load balancer
aws ecs register-task-definition --cli-input-json file://task-definition.json

aws ecs create-service \
  --cluster production \
  --service-name web-app \
  --task-definition web-app \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-abc123],securityGroups=[sg-xyz789],assignPublicIp=ENABLED}" \
  --load-balancers "targetGroupArn=arn:aws:elasticloadbalancingv2:...,containerName=web-app,containerPort=8000"

This is the same container, the same image, the same desired-count: 2 instinct as the Kubernetes Deployments from Week 12 — Fargate is fundamentally the same "run N replicas of this container behind a load balancer" idea, just without a cluster of nodes or a control plane to operate. The trade-off runs the other direction from Kubernetes: less operational surface, but also less of the ecosystem — no Helm charts, no custom Operators, no service mesh sidecar injection.

Choose based on shape and duration, not preference

An event-driven function that runs in milliseconds and idles most of the time belongs on Lambda. A handful of long-running containerized services without the operational need for Kubernetes' extensibility belongs on Fargate. A system genuinely needing StatefulSets, Operators, a service mesh, or portability across clouds belongs on Kubernetes — the right choice tracks the actual workload, and being able to justify it either way is what an interviewer is listening for.

4. Hands-on Exercise

Hands-on

Ship one endpoint on Lambda and one service on Fargate

Build both ends of the serverless spectrum and compare them directly.

Requirements:

  1. Deploy a Lambda function behind an HTTP API Gateway route, and measure its cold-start latency vs. warm-invocation latency with a simple timing script.
  2. Add an S3 event source triggering a second Lambda whenever an object is uploaded to a bucket, and confirm it fires by uploading a test file.
  3. Register a Fargate task definition for a containerized app you built in an earlier week, and run it as an ECS service with 2 desired tasks behind an Application Load Balancer.
  4. Write a one-paragraph comparison: for your specific app, would Lambda, Fargate, or the Kubernetes cluster from Weeks 11–17 be the right production choice, and why?
Hint

CloudWatch Logs Insights query filter @type = "REPORT" | stats avg(@duration), max(@duration) by bin(5m) against your function's log group is a quick way to see cold-start-driven latency spikes without instrumenting anything yourself.

5. Knowledge Check

Three quick questions. Expand each to check your answer.

Q1

Why should a database connection be initialized outside the Lambda handler function, not inside it?

Code at module scope only runs once per execution environment, on a cold start; the handler function runs on every invocation. Opening the connection inside the handler means re-establishing it on every single request even when the environment is warm, adding latency and risking exhausting the database's connection limit under load — initializing it at module scope lets a warm invocation reuse the existing connection.

Q2

With Lambda proxy integration, what is API Gateway's role versus the function's role in shaping the HTTP response?

API Gateway passes the raw request through unchanged and does no transformation of its own; the Lambda function's return value is expected to fully specify the response, including statusCode, headers, and body. This is the opposite of non-proxy integration, where API Gateway itself applies mapping templates to reshape the request and response.

Q3

What's the actual trade-off between choosing Fargate and choosing the Kubernetes cluster from Weeks 11–17 for the same containerized app?

Fargate runs the same container images with far less operational surface — no nodes, no control plane, no cluster to patch — at the cost of losing Kubernetes' extensibility: no Helm charts, custom Operators, or service mesh sidecar injection. The right choice depends on whether the workload genuinely needs that ecosystem (StatefulSets, Operators, portability across clouds) or is better served by fewer moving parts.