← Back to all topics
$ aws lambda invoke --function-name api

Serverless Computing
Instructor Guide

AWS Lambda, API Gateway, DynamoDB, EventBridge — pay nothing when nobody's calling, scale infinitely when everyone is.

01
What Serverless Is — and When to Pick It Over ECS / EC2
"Serverless" doesn't mean no servers. It means you don't manage them — and you only pay for the milliseconds you use.

How to explain to students

Open with: "Imagine renting a server vs renting a taxi. EC2 = your own car (you pay 24/7 even when parked). Lambda = Uber (you pay only the trip)." Both run code. The difference is billing model and operational ownership.

Lambda makes sense when traffic is spiky, low-volume, or event-driven: a webhook handler called 100 times a day, a nightly cron job, an S3-upload thumbnailer. It stops making sense when traffic is steady and high — at ~5 million requests/day, EC2 or ECS becomes cheaper. The other big trade-off is cold starts: a Lambda that hasn't run in 5 minutes takes 200ms–2s to start.

when-to-use-serverless
# Decision matrix

Lambda ECS Fargate EC2
──────────────────────── ──────────────── ─────────────── ───────────────
Idle cost $0 $0–$10 $8+
Pay model per ms + request per vCPU-hour per VM-hour
Scaling instant, ∞ manual + min manual + min
Max execution 15 min unlimited unlimited
Cold starts 200ms–2s 0 0
Best for spiky / events steady containers control / state

# Concrete examples
"Stripe webhook handler called 1k/day" → Lambda ($0.00 idle)
"REST API: 10 req/sec, 24/7" → Fargate ($35/mo, no cold starts)
"GPU video encoder, 5min jobs" → EC2 spot ($/min, custom AMI)
"Resize image when uploaded to S3" → Lambda (event-driven)
"Database that needs to stay up forever" → RDS / Fargate (state)
"Nightly report at 2am" → Lambda + EventBridge
💸
$0 when idle
No requests = no bill. Most learners can fit a small project entirely in the free tier.
⏱️
15-min ceiling
Lambda kills any execution past 15 minutes. Long jobs go elsewhere.
❄️
Cold starts
First call after idle is slow. Provisioned concurrency or warm pings can fix.
🪜
Stateless by design
No local disk, no in-memory sessions. State goes to DynamoDB / S3 / Redis.

🎯 Practice Questions

Q1.
For each, pick Lambda or Fargate and justify in one sentence:
(a) Stripe webhook (50 events/day), (b) Real-time chat (5k concurrent connections), (c) Image thumbnailer triggered by S3 upload, (d) Steady-traffic REST API at 200 RPS.
Show Answer
(a) Stripe webhook → Lambda. 50 events/day = $0/mo on free tier; cold start of 200ms is fine for a webhook.
(b) Real-time chat → Fargate. WebSockets with 5k connections need long-lived processes; Lambda WebSocket support exists but is unwieldy at this scale.
(c) Image thumbnailer → Lambda. Classic event-driven workload, S3 trigger, finishes in seconds.
(d) Steady 200 RPS API → Fargate. 200 × 86,400 = ~17M req/day. Lambda would charge per request and run constantly anyway; Fargate is cheaper at that volume and avoids cold starts.
Q2.
A teammate says "Serverless means no servers." Correct them in one sentence — what does it actually mean?
Q3.
Why is Lambda a poor fit for a long-running ML training job that takes 90 minutes?
💡 Hard timeout limit.
Q4.
Name two ways to mitigate Lambda cold starts when you can't accept a 1-second first-request delay.
02
Lambda Triggers — HTTP, S3, DynamoDB, EventBridge
A Lambda is just a function. Its real power is the variety of things that can call it.

How to explain to students

Mental model: "A Lambda is a vending machine. The trigger is the coin slot — it can be a button (HTTP), a sensor (S3 upload), a timer (cron), or another machine (DynamoDB stream)." The Lambda code doesn't change much. The trigger changes the event payload it receives.

The four canonical triggers cover 80% of real-world serverless: API Gateway (HTTP), S3 (file uploaded), DynamoDB Streams (row changed), EventBridge (schedule or cross-service event). Each delivers a typed event to your handler — read the AWS docs for the exact JSON shape.

handler.js — same function, four triggers
// HTTP via API Gateway
export const handler = async (event) => {
  const { name } = JSON.parse(event.body);
  return { statusCode: 200, body: JSON.stringify({ hello: name }) };
};

// S3 trigger — fires on every upload
export const handler = async (event) => {
  for (const record of event.Records) {
    const bucket = record.s3.bucket.name;
    const key = record.s3.object.key;
    await thumbnailize(bucket, key);
  }
};

// DynamoDB stream — fires on row insert/update/delete
export const handler = async (event) => {
  for (const record of event.Records) {
    if (record.eventName === 'INSERT') {
      await sendWelcomeEmail(record.dynamodb.NewImage);
    }
  }
};

// EventBridge cron — runs on a schedule
export const handler = async (event) => {
  // event has no payload here — just metadata
  await runNightlyReport();
};

# Wire EventBridge to fire daily at 2am UTC
$ aws events put-rule --name nightly-report \
  --schedule-expression "cron(0 2 * * ? *)"
$ aws events put-targets --rule nightly-report \
  --targets Id=1,Arn=arn:aws:lambda:eu-west-1:123:function:nightly-report
🌐
API Gateway
HTTP/HTTPS in, JSON out. The "REST API" trigger.
🗄️
S3 events
Upload, delete, multi-part complete. Best for ETL + file processing.
📡
DynamoDB streams
Row-change events. Send a welcome email on user signup, etc.
EventBridge cron
Scheduled triggers. cron syntax. Lambdas as cron jobs.

🎯 Practice Questions

Q1.
Pick the right trigger for each: (a) "send welcome email when user signs up", (b) "generate thumbnail on photo upload", (c) "check if SSL cert expires in 30 days, every Monday".
Q2.
Why does S3 trigger send multiple records per invocation, not just one?
Show Answer
S3 batches events for performance. If a user uploads 10 files in quick succession, Lambda may receive a single invocation with 10 Records in the event payload — instead of 10 invocations. This is intentional: it reduces cold-start overhead and keeps cost down.

Implication: always loop over event.Records in S3 handlers. Treating event.Records[0] as if it were the only record will silently drop events under load.
Q3.
Write the EventBridge cron expression for "every Monday at 9:00 AM UTC".
💡 EventBridge uses 6-field cron, not standard 5-field.
Q4.
Why use a DynamoDB Stream instead of having the API call sendEmail() directly inside the signup handler?
03
REST APIs with API Gateway + Lambda
Building a real REST API with paths, methods, validation, and CORS

How to explain to students

API Gateway sits in front of Lambda and turns HTTP into Lambda invocations. You define routes (POST /items, GET /items/{id}) in API Gateway; each route is wired to a Lambda function. API Gateway handles authentication, rate-limiting, and CORS.

Two flavours: REST API (the original, more features but more expensive) and HTTP API (newer, cheaper, simpler — start here unless you need the REST features). For most learners, HTTP API is the right default.

items-api — full Lambda + API Gateway
// handler.ts — single Lambda routes 3 endpoints
import { APIGatewayProxyEventV2 } from 'aws-lambda';

export const handler = async (event: APIGatewayProxyEventV2) => {
  const { method, path } = event.requestContext.http;
  const route = `${method} ${path}`;

  if (route === 'GET /items') return list();
  if (route === 'POST /items') return create(JSON.parse(event.body!));
  if (route.startsWith('GET /items/'))
    return get(event.pathParameters!.id!);

  return { statusCode: 404, body: JSON.stringify({ error: 'Not Found' }) };
};

# Create the HTTP API in 2 commands
$ aws apigatewayv2 create-api --name items-api \
  --protocol-type HTTP \
  --target arn:aws:lambda:eu-west-1:123:function:items-handler
"ApiId": "abc123", "ApiEndpoint": "https://abc123.execute-api.eu-west-1.amazonaws.com"

# Test it
$ curl -X POST https://abc123.execute-api.eu-west-1.amazonaws.com/items \
  -H 'content-type: application/json' \
  -d '{"name":"Wireless Mouse","price":2400}'
{"id":"itm_1a2b3c","name":"Wireless Mouse","price":2400}
HTTP API REST API Lambda proxy CORS JWT authorizer stages

🎯 Practice Questions

Q1.
A frontend at app.example.com calls your API at api.example.com and gets a CORS error. List the two places the fix can live and the trade-off between them.
Show Answer
Option A — fix in API Gateway (recommended): configure CORS on the HTTP API itself. API Gateway returns the right Access-Control-Allow-Origin headers; your Lambda doesn't need to know about CORS at all.

Option B — fix in Lambda: return CORS headers from every handler. Works, but easy to miss on a route, and adds CORS logic to every function.

Trade-off: Option A is centralised + DRY but adds API Gateway-specific config. Option B is portable but bug-prone. Pick A unless you have a specific reason.
Q2.
When should you split POST /items and GET /items into two separate Lambdas instead of one with internal routing?
Q3.
Pick HTTP API or REST API for: (a) a public webhook endpoint, (b) an internal API needing usage plans + API keys per consumer, (c) a JSON CRUD app for a side project.
💡 HTTP API = cheaper + simpler. REST API = more features (usage plans, X-Ray, request/response transformations).
Q4.
Your API returns 502 Bad Gateway. The Lambda runs fine standalone. Where do you look first?
04
DynamoDB & S3 as Storage for Serverless Apps
A serverless API needs serverless storage — DynamoDB for structured data, S3 for blobs

How to explain to students

Don't put a Lambda in front of RDS for first projects. RDS = always-on database = 24/7 cost = defeats the serverless free-tier dream. DynamoDB is AWS's serverless key-value/document store — pay-per-request, autoscales, never down. Combined with S3 for files, you can build a complete serverless app at $0 idle.

DynamoDB has a learning curve — there are no joins, indexing is deliberate, and you design tables around your access patterns, not the data shape. Start with the simplest one-table design.

dynamodb + s3 — read/write
# Create a DynamoDB table — pay-per-request, no idle cost
$ aws dynamodb create-table \
  --table-name items \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

# In Lambda — write + read
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb';

const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));

async function create(item) {
  const id = `itm_${crypto.randomUUID().slice(0, 8)}`;
  await ddb.send(new PutCommand({
    TableName: 'items',
    Item: { id, ...item, createdAt: Date.now() }
  }));
  return { statusCode: 201, body: JSON.stringify({ id, ...item }) };
}

# Upload binary to S3 from Lambda
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({});
await s3.send(new PutObjectCommand({
  Bucket: 'my-uploads', Key: `images/${id}.jpg`, Body: buffer
}));
💵
Pay-per-request
DynamoDB PAY_PER_REQUEST mode. $0 idle, scales to whatever traffic comes.
🔑
Single-table design
Advanced pattern: many entity types in one table. Start simple.
📂
S3 for blobs
Images, files, exports. Don't store binary in DynamoDB.
🌊
Streams = events
DynamoDB Streams turn every write into an event Lambda can consume.

🎯 Practice Questions

Q1.
A student picks RDS Postgres for their serverless side project to "stick with what they know." Why might this destroy their AWS bill?
Q2.
Why is "design for your access patterns" the DynamoDB rule, vs the SQL rule of "design around the data"?
Show Answer
In SQL, you normalise data and write queries (joins, where) at read time. The database does the work.

DynamoDB has no joins. Every query reads at most one partition key + sort key range. So you design the table so the queries you'll run map directly onto a single partition lookup. This often means denormalising — storing the same data in multiple shapes — and pre-computing relationships at write time.

The mantra: "What questions will I ask of this data?" before you design the table — opposite of SQL where you can usually re-shape queries later.
Q3.
Your handler uploads an image to S3 then writes a row to DynamoDB. What happens if the DynamoDB write fails after the S3 upload succeeds? How do you make this safer?
💡 Two-phase commits don't exist on AWS — but idempotency does.
Q4.
Why is putting binary image data inside a DynamoDB row a poor design, even though DynamoDB technically supports binary attributes up to 400 KB?
05
Serverless Framework — Deploy without Wiring 20 Things by Hand
One YAML file describes your Lambdas, triggers, IAM, and env vars. serverless deploy ships it all.

How to explain to students

Wiring a Lambda + API Gateway + DynamoDB + IAM by hand takes 20+ AWS CLI commands and is impossible to reproduce. The Serverless Framework (or AWS SAM, or AWS CDK) describes everything in one file and generates the underlying CloudFormation. The community-favourite is Serverless Framework — readable YAML, tons of plugins, multi-cloud.

It also handles environments: a single command can deploy to dev / staging / prod with different env vars, IAM roles, and table names. Critical once your project leaves your laptop.

serverless.yml — full app in 30 lines
service: items-api

provider:
  name: aws
  runtime: nodejs20.x
  region: eu-west-1
  stage: ${opt:stage, 'dev'}  # dev / staging / prod
  environment:
    TABLE_NAME: items-${self:provider.stage}
  iam:
    role:
      statements:
        - Effect: Allow
          Action: [dynamodb:GetItem, dynamodb:PutItem, dynamodb:Scan]
          Resource: !GetAtt ItemsTable.Arn

functions:
  api:
    handler: src/handler.handler
    events:
      - httpApi: 'GET /items'
      - httpApi: 'POST /items'
      - httpApi: 'GET /items/{id}'

  nightly:
    handler: src/cron.report
    events:
      - schedule: cron(0 2 * * ? *)

resources:
  Resources:
    ItemsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: items-${self:provider.stage}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions: [{ AttributeName: id, AttributeType: S }]
        KeySchema: [{ AttributeName: id, KeyType: HASH }]

# Deploy
$ npx serverless deploy --stage dev
✔ Service deployed to stack items-api-dev (38s)
endpoint: https://abc.execute-api.eu-west-1.amazonaws.com
serverless deploy --stage CloudFormation SAM CDK

🎯 Practice Questions

Q1.
Compare Serverless Framework, AWS SAM, and AWS CDK in one line each. Which is closest to "Terraform but for serverless"?
Show Answer
Serverless Framework — multi-cloud YAML, plugin-rich, community favourite for Lambda apps.
AWS SAM — AWS-only YAML, thin wrapper over CloudFormation, official AWS support.
AWS CDK — write infra in TypeScript/Python/Go (real code, not YAML). Compiles to CloudFormation.

"Terraform but for serverless" → AWS CDK is closest in spirit (real code, declarative). But Serverless Framework is closer in scope — both are higher-level abstractions tuned for serverless apps. Terraform itself works fine for serverless too, just with more boilerplate.
Q2.
Why is having ${self:provider.stage} in the table name items-dev / items-staging / items-prod important?
Q3.
A teammate runs serverless deploy with no --stage flag and accidentally deploys to prod. How would you prevent this?
06
Using AI for Serverless — Handlers, IAM, and Cost Estimates
Lambda handlers are short, pattern-heavy, and AI-friendly — but verify the IAM

How to explain to students

AI can produce a high-quality Lambda handler in seconds — these functions are usually 20–50 lines, follow well-known patterns (parse event, do work, return response), and have lots of training data. AI is also strong at IAM policies and serverless.yml YAML.

The trap: AI sometimes generates over-permissive IAM (everyone gets "Action": "*") and forgets edge cases (no input validation, no error handler). Treat the output as a draft and review every IAM statement before deploying.

AI prompts for serverless
# ✅ Strong handler prompt
"Write an AWS Lambda handler in TypeScript using Node 20 that:
- Reads an S3 upload event
- Generates a 200x200 thumbnail using sharp
- Writes the thumbnail back to the same bucket under thumbs/<original-key>
- Loops over event.Records (S3 batches events)
- Skips files larger than 10 MB
- Logs structured JSON to CloudWatch
- Returns nothing on success, throws on unrecoverable error
After the handler, list the minimum IAM policy + a serverless.yml snippet."

# Cost-estimate prompt
"Estimate the monthly AWS cost for this serverless workload:
- 100,000 Lambda invocations/month (avg 200ms, 256 MB memory)
- 50,000 DynamoDB writes + 200,000 reads (PAY_PER_REQUEST)
- 5 GB S3 storage, 100 GB egress through CloudFront
Show the math, region eu-west-1, current AWS pricing."

# Verify AI-generated IAM
$ aws iam simulate-custom-policy \
  --policy-input-list file://generated-policy.json \
  --action-names s3:GetObject s3:PutObject \
  --resource-arns "arn:aws:s3:::my-bucket/*"

🎯 Practice Questions

Q1.
Take "write me a Lambda for image upload" and turn it into a 6-bullet detailed prompt that produces a production-ready handler.
Q2.
Why should you ask AI for IAM policies and verify them with iam simulate-custom-policy instead of just trusting the output?
Show Answer
AI is statistically right but sometimes silently wrong about IAM. Three failure modes:
1. Hallucinated action names. s3:GetAllObjects, dynamodb:WriteAll — sound real, don't exist.
2. Over-permissive scopes. AI defaults to "Resource": "*" when one specific ARN would do. Works, then fails the security review.
3. Wrong resource ARN format. S3 actions need both arn:aws:s3:::bucket AND arn:aws:s3:::bucket/*. AI sometimes only includes one.

iam simulate-custom-policy takes the policy + a list of actions + ARNs and tells you which would be allowed/denied. Run it before deploy — it's free and prevents the "policy has no effect, why doesn't this work" debugging session.
Q3.
An AI says "your Lambda will cost $0/month on the free tier." Why is this often misleading for production estimates? List two costs the free tier doesn't cover.
07
Project: Serverless REST API — Lambda + API Gateway + DynamoDB
A complete CRUD API deployed end-to-end with the Serverless Framework

How to explain to students

Walk through this on screen first, then have students recreate it for their own domain (todos, bookmarks, notes — anything CRUD-shaped). The artefact: a public REST API at https://<random>.execute-api.eu-west-1.amazonaws.com, talking to DynamoDB, deployed via one command, costing $0/mo idle.

items-api/ — file layout
items-api/
├── serverless.yml
├── package.json
├── tsconfig.json
└── src/
├── handler.ts ← all routes
├── repo.ts ← DynamoDB read/write
└── validate.ts ← zod schemas
src/handler.ts
import type { APIGatewayProxyEventV2 as Event } from 'aws-lambda';
import { repo } from './repo.js';
import { ItemSchema } from './validate.js';

const ok = (body: unknown, status = 200) => ({
  statusCode: status,
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify(body),
});

export const handler = async (event: Event) => {
  const { method, path } = event.requestContext.http;

  if (method === 'GET' && path === '/items')
    return ok(await repo.list());

  if (method === 'POST' && path === '/items') {
    const parsed = ItemSchema.safeParse(JSON.parse(event.body ?? '{}'));
    if (!parsed.success) return ok({ error: parsed.error }, 400);
    return ok(await repo.create(parsed.data), 201);
  }

  if (method === 'GET' && path.startsWith('/items/')) {
    const item = await repo.get(event.pathParameters!.id!);
    return item ? ok(item) : ok({ error: 'Not Found' }, 404);
  }

  return ok({ error: 'Route not found' }, 404);
};

# Deploy
$ npx serverless deploy --stage dev
endpoint: https://abc.execute-api.eu-west-1.amazonaws.com

# Smoke test
$ curl -X POST $URL/items -H 'content-type: application/json' \
  -d '{"name":"Mouse","price":2400}'
{"id":"itm_a3f1c2","name":"Mouse","price":2400}
🧪
zod validation
Type-safe runtime input validation. Reject malformed bodies with 400.
🗄️
repo pattern
Keep DynamoDB calls in one file. Handler stays HTTP-shaped.
🪜
stages from day 1
--stage dev / --stage prod. Different tables, different env.
📐
Stateless
No /tmp persistence, no in-memory cache. Lambda restarts erase both.
08
Quiz: Serverless Concepts, Lambda Triggers & Use Cases
5 MCQs + 2 fill-in-the-command questions

Sample quiz questions (interactive)

Q1. The Lambda execution time limit is:
A
30 seconds
B
5 minutes
C
15 minutes
D
Unlimited
Q2. A Lambda + DynamoDB serverless app costs how much when nobody is calling it?
A
$0 (pay-per-request)
B
$5/month minimum
C
$30/month minimum
D
Same as an EC2 t3.micro
Q3. The "right" trigger for "send email when row inserted into users table" is:
A
EventBridge cron
B
S3 event
C
DynamoDB Stream
D
API Gateway
Q4. For a steady 200 RPS API running 24/7, the cheaper choice is:
A
Lambda + API Gateway
B
ECS Fargate or EC2
C
Two Lambdas in failover
D
Always Lambda — it's cheaper
Q5. What does an HTTP API (in API Gateway) give you that a REST API doesn't natively?
A
Usage plans + API keys
B
Lower cost + simpler config
C
X-Ray tracing
D
Request validators

Fill-in-the-command

Fill 1: Cron expression for "every day at 2:30 AM UTC" in EventBridge.
Fill 2: Deploy your serverless project to a stage called prod.
09
Assignment: Cost Comparison — S3 vs EC2 vs Lambda for a Low-Volume Service
A 1-page write-up that picks the right architecture for a real-world workload

How to explain to students

Frame as a cost-engineering memo: "You've been asked to host the company's contact-form backend (50 submissions/day, mostly business hours). Compare three architectures and recommend one with numbers. Your manager doesn't have time to read more than one page." This forces students to think about cost per request, idle cost, and operational overhead.

📋 Assignment Requirements

  • Workload spec (use this exactly): "A contact form backend receiving 50 POST requests/day on weekdays (avg 200ms, 256 MB), storing each submission in a database, sending an email via SES on each submission, plus a static HTML status page served at /."
  • Compare three architectures: (a) S3 + Lambda + DynamoDB + SES, (b) EC2 t3.micro + nginx + Postgres + SES, (c) Beanstalk + RDS + SES
  • For each architecture: monthly cost breakdown (compute, storage, network, SES) using current AWS pricing
  • Identify the AWS Free Tier coverage for each (be honest — some hide hidden costs after 12 months)
  • Compare on three axes: cost, operational overhead, scalability headroom
  • Include a 1-paragraph recommendation. Justify with at least 2 numeric arguments.
  • Include 1 architecture diagram (hand-drawn, draw.io, or Excalidraw) for the recommended option
  • Bonus: Pick a 100×-traffic version (5,000 submissions/day) — does the recommendation change? Why?
  • Bonus: A 1-page tear-down script or reverse architecture diagram showing what to delete to stop billing
expected output — sample table
# 1500 invocations/month, 256 MB, 200ms each

S3+Lambda EC2 t3.micro Beanstalk+RDS
──────────────── ───────────── ─────────────── ──────────────
Compute ~$0.00 $7.50 $7.50 (EC2)
Storage $0.10 (S3) $1.00 (EBS) $14.00 (RDS)
DB $0.00 self-hosted bundled above
SES (1500 emails) $0.15 $0.15 $0.15
Egress ~free ~free ~free
──────────────── ───────────── ─────────────── ──────────────
Monthly total ~$0.25 ~$8.65 ~$21.65
Ops overhead LOW HIGH (patches) MED (managed)
Cold start risk YES none none
📊
Grading rubric
Cost numbers correct: 30. Trade-offs explained: 25. Recommendation justified: 20. Diagram clear: 15. Stretch: 10.
🎯
Common mistakes
Forgetting NAT gateway costs ($32/mo!), missing SES reputation impact, hand-waving "Lambda is cheaper" without numbers.
💡
Stretch
Add a 4th architecture (App Runner or ECS Fargate Spot). Compare against the same workload.