What is AWS Lambda?
AWS Lambda runs your code in short‑lived, auto‑scaling compute without managing servers. You upload a handler, choose memory/timeout, and Lambda executes it on demand in response to events (HTTP requests, SQS messages, S3 uploads, schedules, etc.). You pay only for invocations and compute time in milliseconds.
Core ideas:
- Event‑driven: Lambda is triggered by events from AWS services or your app.
- Stateless: Each invocation is isolated. Use external storage (DynamoDB, S3) for state.
- Ephemeral: You get temporary storage (/tmp) and a short execution window (up to 15 minutes).
- Scales automatically: Concurrency grows with traffic; you can set limits.
Minimal Lambda handlers
Node.js (TypeScript/JavaScript):
// handler.ts
export const handler = async (event: any) => {
console.log("event", JSON.stringify(event));
return {
statusCode: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({ ok: true }),
};
};
Python:
# handler.py
import json
def handler(event, context):
print("event", event)
return {
"statusCode": 200,
"headers": {"content-type": "application/json"},
"body": json.dumps({"ok": True})
}
Working with SST (Serverless Stack)
SST makes it easy to build, test, and deploy Lambda apps with great DX: local debugging, file‑based routing, live Lambda dev, and simple constructs for APIs, queues, cron jobs, and databases.
Quick start
Prereqs: Node 18+, AWS credentials.
npm create sst@latest my-lambda-app
cd my-lambda-app
npm install
Your project will have:
- sst.config.ts – app config and stacks
- stacks/ – infrastructure (API, functions, tables, queues)
- packages/functions/ – Lambda source code
Define an API with a Lambda route
stacks/MyStack.ts
import { StackContext, Api, Table } from "sst/constructs";
export function MyStack({ stack }: StackContext) {
// DynamoDB table
const table = new Table(stack, "table", {
fields: { pk: "string" },
primaryIndex: { partitionKey: "pk" },
});
// API Gateway + Lambda
const api = new Api(stack, "api", {
defaults: {
function: {
runtime: "nodejs20.x",
memorySize: 128,
architecture: "arm_64", // cheaper & usually faster
bind: [table], // grants IAM + env vars
},
},
routes: {
"GET /": "packages/functions/src/get.handler",
"POST /items": "packages/functions/src/create.handler",
},
});
stack.addOutputs({ ApiUrl: api.url });
}
packages/functions/src/get.ts
import { Table } from "sst/node/table";
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
const ddb = new DynamoDBClient({});
export const handler = async () => {
const tableName = Table.table.tableName; // from bind
const res = await ddb.send(new GetItemCommand({
TableName: tableName,
Key: { pk: { S: "hello" } },
}));
return {
statusCode: 200,
body: JSON.stringify({ item: res.Item ?? null }),
};
};
packages/functions/src/create.ts
import { Table } from "sst/node/table";
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
const ddb = new DynamoDBClient({});
export const handler = async (event: any) => {
const { value } = JSON.parse(event.body || "{}");
const tableName = Table.table.tableName;
await ddb.send(new PutItemCommand({
TableName: tableName,
Item: { pk: { S: "hello" }, value: { S: String(value) } },
}));
return { statusCode: 201, body: JSON.stringify({ ok: true }) };
};
Run locally with live Lambda dev:
npm run dev # or: sst dev
# SST will give you a URL. Hitting it invokes your code locally while AWS handles the event plumbing.
Deploy:
npm run deploy # or: sst deploy
Remove:
sst remove
Add a scheduled job (Cron)
stacks/MyStack.ts
import { Cron } from "sst/constructs";
new Cron(stack, "nightly", {
schedule: "cron(0 3 * * ? *)", // 3:00 AM UTC daily
job: {
function: {
handler: "packages/functions/src/nightly.handler",
memorySize: 256,
},
},
});
packages/functions/src/nightly.ts
export const handler = async () => {
console.log("Running nightly maintenance...");
// do work
};
Add a queue worker (SQS)
stacks/MyStack.ts
import { Queue } from "sst/constructs";
const queue = new Queue(stack, "jobs", {
consumer: {
function: {
handler: "packages/functions/src/worker.handler",
timeout: "30 seconds",
memorySize: 256,
},
},
});
packages/functions/src/worker.ts
export const handler = async (event: any) => {
for (const record of event.Records) {
const msg = JSON.parse(record.body);
console.log("Processing", msg);
}
};
Tuning Lambda for performance and cost
Key settings per function:
- Memory size: Higher memory = more CPU and network. Tune for fastest total time at lowest cost.
- Architecture: arm_64 (Graviton) is typically cheaper and faster than x86_64.
- Timeout: Set near your p95 to catch hangs and avoid partial work.
- Concurrency: Use reserved concurrency to cap costs; provisioned concurrency to smooth cold starts (cost trade‑off).
- VPC: Only use if needed (RDS/ElastiCache). It adds latency and can introduce NAT costs.
With SST, you set these on the construct:
new Api(stack, "api", {
defaults: { function: { memorySize: 256, timeout: 10, architecture: "arm_64" } },
routes: { "GET /": "packages/functions/src/handler.main" },
});
How the Lambda execution model impacts design
- Cold starts: First request or scaled‑out concurrency may incur a startup delay. Keep bundles small, avoid huge dependencies, and consider provisioned concurrency for latency‑sensitive endpoints.
- Idempotency: Retries happen (e.g., SQS, EventBridge). Make handlers idempotent.
- Batching: For SQS/Kinesis, process in batches to reduce cost and improve throughput.
- Observability: Use CloudWatch Logs and X‑Ray. SST surfaces logs and tailing in the console.
Cost: what you actually pay and why it’s efficient
Lambda pricing has two parts:
- Requests: $0.20 per 1M requests (first 1M/month free)
- Compute: ~$0.0000166667 per GB‑second on x86 (first 400k GB‑seconds/month free). Graviton (arm64) compute is discounted.
You’re billed per millisecond of execution, based on the memory you configure. You don’t pay for idle time or capacity planning.
Cost examples
-
Light API: 128 MB, 50 ms average, 5M requests/month
- GB‑seconds = 0.125 GB × 0.05 s × 5,000,000 = 31,250 GB‑s
- Compute cost ≈ 31,250 × 0.0000166667 = $0.52
- Requests = 5M × $0.20/1M = $1.00
- Total ≈ $1.52/month before free tier
- With free tier (1M req + 400k GB‑s): compute becomes $0, requests drop to $0.80 → ≈ $0.80/month
-
Heavier workload: 512 MB, 200 ms, 20M requests/month
- GB‑seconds = 0.5 × 0.2 × 20,000,000 = 2,000,000 GB‑s
- Compute ≈ 2,000,000 × 0.0000166667 = $33.33
- Requests = 20M × $0.20/1M = $4.00
- Total ≈ $37.33/month (before tiered/arm64 discounts)
Takeaways:
- Short, memory‑efficient functions are incredibly cheap at scale.
- The free tier meaningfully offsets small and dev workloads.
- Graviton can reduce compute cost further.
When costs can rise
- Provisioned Concurrency: You pay to keep instances warm. Pricing is roughly $0.015 per GB‑hour plus your normal compute. Example: 512 MB, 10 provisioned, ~730 hours/month → 0.5 GB × 10 × 730 × $0.015 ≈ $54.75/month just for keeping them warm.
- API Gateway, DynamoDB, and CloudWatch Logs can dominate total cost. Prefer HTTP API over REST API when possible, use structured logging, and set log retention.
- NAT Gateway: If you put Lambdas in a private subnet and egress via NAT, you can rack up data processing fees.
Practical cost optimization checklist
- Choose arm_64 and benchmark. Many Node/Python apps get lower latency and cost.
- Tune memory with AWS Lambda Power Tuning (Step Functions blueprint). Find the sweet spot of time vs. price.
- Keep bundles small: tree‑shake, avoid giant SDKs, use AWS SDK v3 modular clients.
- Use async event sources (SQS/EventBridge) for spiky workloads to avoid API cold‑start sensitivity.
- Avoid VPC unless necessary. If needed, use VPC endpoints to minimize NAT costs.
- Right‑size concurrency. Use reserved concurrency to cap spend; only use provisioned concurrency for p95 latency SLOs.
- Choose API Gateway HTTP APIs for most REST workloads—they’re simpler and cheaper.
Local dev, testing, and debugging with SST
- Live Lambda dev:
sst devruns your code locally while invoking through AWS. Fast feedback, real infra. - Unit tests: Keep handlers thin; extract pure functions and test them directly.
- Logs:
sst devstreams CloudWatch logs in your terminal. - Env & secrets: Use
bindfor infra resources; use AWS Secrets Manager or Parameter Store for secrets.
Putting it all together
Lambda lets you scale to zero and pay only for what you use. SST gives you a smooth developer experience—simple constructs, live debugging, and safe deploys. Combined, you can ship production APIs, workers, and scheduled jobs quickly and keep costs tiny, even at large request volumes.
Next steps:
- Bootstrap a new app:
npm create sst@latestand add an Api + Table - Add a Cron and a Queue
- Benchmark memory/arch and verify cost with the examples above
- Set log retention and watch your non‑Lambda costs (API Gateway, logs, data egress)
Happy shipping!