AWS Lambda
Table of Contents
AWS Lambda lets you run code without managing servers. You upload a function, define what triggers it (an HTTP request, a file upload, a schedule), and AWS handles everything else — provisioning, scaling, patching, and shutting down when idle. You only pay for the milliseconds your code actually runs.
If you’ve been through the EC2 Basics tutorial, you know that EC2 gives you full virtual machines. Lambda is the opposite end of the spectrum: no servers to manage at all.
Key Concepts
- Function — Your code, packaged and deployed to Lambda
- Trigger — What causes your function to run (API Gateway, S3 event, schedule, etc.)
- Event — The input data passed to your function when triggered
- Execution environment — The container Lambda creates to run your code
- Cold start — The delay when Lambda creates a new execution environment (first invocation or after idle time)
Creating Your First Lambda Function
Via the AWS Console
- Open the Lambda console → Create function
- Choose Author from scratch
- Name:
hello-world - Runtime: Node.js 20.x
- Click Create function
Replace the default code with:
export const handler = async (event) => {
const name = event.queryStringParameters?.name || "World";
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: `Hello, ${name}!` })
};
};
Click Deploy, then Test with a test event to verify it works.
Via the AWS CLI
# Create a file called index.mjs
cat > index.mjs << 'EOF'
export const handler = async (event) => {
const name = event.queryStringParameters?.name || "World";
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: `Hello, ${name}!` })
};
};
EOF
# Zip it
zip function.zip index.mjs
# Create the function (replace the role ARN with yours)
aws lambda create-function \
--function-name hello-world \
--runtime nodejs20.x \
--handler index.handler \
--zip-file fileb://function.zip \
--role arn:aws:iam::123456789012:role/lambda-execution-role
# Test it
aws lambda invoke \
--function-name hello-world \
--payload '{"queryStringParameters": {"name": "Alice"}}' \
output.json
cat output.json
Adding an API Gateway Trigger
To make your Lambda accessible via HTTP:
- In the Lambda console, click Add trigger
- Select API Gateway
- Choose HTTP API (simpler and cheaper than REST API)
- Security: Open (for testing; use IAM or JWT for production)
- Click Add
You’ll get a URL like https://abc123.execute-api.us-east-1.amazonaws.com/hello-world. Hit it in your browser:
https://abc123.execute-api.us-east-1.amazonaws.com/hello-world?name=Alice
→ {"message": "Hello, Alice!"}
The Event Object
The event parameter contains different data depending on the trigger:
API Gateway event
{
"httpMethod": "GET",
"path": "/hello-world",
"queryStringParameters": { "name": "Alice" },
"headers": { "content-type": "application/json" },
"body": null
}
S3 event (file uploaded)
{
"Records": [{
"s3": {
"bucket": { "name": "my-bucket" },
"object": { "key": "uploads/photo.jpg", "size": 1024 }
}
}]
}
Scheduled event (EventBridge/CloudWatch)
{
"source": "aws.events",
"detail-type": "Scheduled Event",
"time": "2026-05-18T14:00:00Z"
}
Practical Examples
Process uploaded images
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({});
export const handler = async (event) => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = record.s3.object.key;
console.log(`Processing: s3://${bucket}/${key}`);
const response = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
// Process the file...
}
return { statusCode: 200, body: "Processed" };
};
Scheduled cleanup task
import { DynamoDBClient, ScanCommand, DeleteItemCommand } from "@aws-sdk/client-dynamodb";
const dynamo = new DynamoDBClient({});
const TABLE = process.env.TABLE_NAME;
export const handler = async () => {
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
const { Items } = await dynamo.send(new ScanCommand({
TableName: TABLE,
FilterExpression: "expiresAt < :cutoff",
ExpressionAttributeValues: { ":cutoff": { N: String(thirtyDaysAgo) } }
}));
for (const item of Items || []) {
await dynamo.send(new DeleteItemCommand({
TableName: TABLE,
Key: { id: item.id }
}));
}
console.log(`Deleted ${Items?.length || 0} expired items`);
};
REST API with multiple routes
export const handler = async (event) => {
const { httpMethod, path, body } = event;
if (httpMethod === "GET" && path === "/users") {
return respond(200, await getUsers());
}
if (httpMethod === "POST" && path === "/users") {
const userData = JSON.parse(body);
return respond(201, await createUser(userData));
}
return respond(404, { error: "Not found" });
};
function respond(statusCode, body) {
return {
statusCode,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
};
}
Environment Variables
Store configuration without hardcoding:
aws lambda update-function-configuration \
--function-name my-function \
--environment "Variables={DB_HOST=mydb.cluster.us-east-1.rds.amazonaws.com,TABLE_NAME=users}"
Access them in code:
const dbHost = process.env.DB_HOST;
const tableName = process.env.TABLE_NAME;
Configuration
Memory and timeout
aws lambda update-function-configuration \
--function-name my-function \
--memory-size 512 \
--timeout 30
- Memory: 128 MB to 10,240 MB. CPU scales proportionally with memory.
- Timeout: Maximum 15 minutes. Set it to slightly more than your function needs.
Layers (shared dependencies)
Package shared libraries as layers to keep function packages small:
# Create a layer with node_modules
mkdir -p nodejs
cp -r node_modules nodejs/
zip -r layer.zip nodejs
aws lambda publish-layer-version \
--layer-name my-dependencies \
--zip-file fileb://layer.zip \
--compatible-runtimes nodejs20.x
Cold Starts
When Lambda creates a new execution environment, there’s a delay (cold start):
- Node.js/Python: 100–500ms typically
- Java/.NET: 1–5 seconds (JVM/CLR startup)
Mitigation strategies:
- Keep functions small (less code to load)
- Use provisioned concurrency for latency-sensitive functions
- Initialize SDK clients outside the handler (reused across invocations)
// Good — client created once, reused across invocations
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({});
export const handler = async (event) => {
// client is already initialized on warm invocations
// ...
};
Pricing
Lambda pricing has two components:
- Requests: $0.20 per 1 million requests
- Duration: $0.0000166667 per GB-second
The free tier includes 1 million requests and 400,000 GB-seconds per month — enough for most development and small production workloads.
When to Use Lambda vs EC2
| Use Lambda | Use EC2 |
|---|---|
| Event-driven workloads | Long-running processes |
| Unpredictable traffic | Steady, high traffic |
| Short tasks (< 15 min) | Tasks longer than 15 minutes |
| Don’t want to manage servers | Need full OS control |
| Pay-per-use makes sense | Reserved instances are cheaper at scale |
What’s Next
Lambda is the foundation of serverless architecture on AWS. From here, explore API Gateway for building full REST APIs, DynamoDB for serverless databases, or Step Functions for orchestrating multiple Lambda functions into workflows.