Event-driven compute with no server to manage.
About this module
Here the paradigm shifts: no instances, no PaaS. You write functions, define the event that triggers them, and AWS runs them when needed. We'll replace the Inventory consumer with Lambda and add functions for emails, reports, and more.
Level: Intermediate · Estimated duration: 4-5 hours
Contents
- Lambda in 5 minutes
- Anatomy of a function
- Triggers: how Lambda is invoked
- Execution models
- Cold start and optimization
- Memory, timeout, and cost
- Permissions and IAM
- Migrating the Inventory consumer
- Additional project functions
- Layers and dependencies
- CloudFormation templates
- Practice exercises
- Next steps
01. Lambda in 5 minutes
Lambda is the most transformative AWS service for many teams. It's worth understanding at a high level before diving into the details.
The core idea
You write a function (literally: a function in code) and hand it to AWS. You configure an event that triggers the function (a message in a queue, a file in S3, an HTTP request, a schedule). AWS runs it when the event arrives. You pay only for execution time.
There's no server to manage. No OS to patch. No scaling to configure. AWS handles everything.
What changes in the mental model
- It's not a "small server" — it's a different model, with its own characteristics.
- Stateless — each execution is independent; there's no state between calls.
- Time-limited — a maximum of 15 minutes per execution.
- Size-limited — up to 250 MB of code (with layers).
- Cold starts — the first execution has additional latency.
- Managed concurrency — AWS scales automatically up to configured limits.
When Lambda shines
- Event-driven tasks — "when X happens, do Y".
- Intermittent traffic — peaks during the day, zero at night.
- Glue code — connecting AWS services (S3 → DynamoDB, etc).
- Queue workers — replacing SQS polling.
- Simple APIs — via API Gateway + Lambda.
When NOT to use Lambda
- Constant, high traffic — can end up more expensive than EC2/Beanstalk.
- Long-running processing — > 15 min, use Step Functions or ECS.
- Latency-critical workloads — cold start can add 100-1000ms.
- Stateful workloads — Lambda isn't the right tool.
02. Anatomy of a function
Every Lambda function has the same basic structure, regardless of language.
The components
- Handler — the entry function that Lambda invokes. Receives event and context.
- Runtime — the execution environment: Node.js 20, Python 3.12, Java 17, Go, etc.
- Configuration — memory, timeout, env vars, IAM role, VPC, layers.
- Trigger — what invokes the function (manual, S3, SQS, EventBridge, etc).
- Layers — code shared across functions (libs, dependencies).
Code structure (Node.js)
// index.js (structure only — you implement the logic)
export const handler = async (event, context) => {
console.log('Event:', JSON.stringify(event));
// Your logic here
// - Process the event
// - Access other services (DynamoDB, SES, S3...)
// - Return a response
return {
statusCode: 200,
body: JSON.stringify({ message: 'ok' })
};
};The event object
Each trigger type sends an event with a different format. For SQS:
{
"Records": [
{
"messageId": "...",
"receiptHandle": "...",
"body": "{\"orderId\": \"ord-123\", ...}",
"attributes": { ... },
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:...",
"awsRegion": "us-east-1"
}
]
}Records is an array — Lambda can receive up to 10 messages in a batch (configurable). Your function processes them all and returns OK only if all of them passed. If any fail, configure partial batch response.
The context object
context = {
awsRequestId: 'unique-id-da-execução',
functionName: 'inventory-consumer',
functionVersion: '1',
invokedFunctionArn: 'arn:aws:lambda:...',
memoryLimitInMB: '256',
getRemainingTimeInMillis: () => Number,
// ... more fields
}03. Triggers: how Lambda is invoked
Each trigger type has its own characteristics — event format, invocation model, guarantees. Knowing which one to use is an important part of the design.
The main triggers
| Trigger | When to use |
|---|---|
| API Gateway | HTTP/REST/WebSocket APIs |
| SQS | Consume queue messages (replaces polling) |
| SNS | React to pub/sub notifications |
| S3 | Process files: upload, delete, etc |
| DynamoDB Streams | React to table changes |
| EventBridge | Schedule (cron) or custom events |
| CloudWatch Logs | Process/forward logs |
| Kinesis | Data streaming |
Invocation models
Synchronous
API Gateway → Lambda. The client waits for the return. Errors go back to the client. No automatic retry.
Asynchronous
S3 → Lambda, SNS → Lambda. Lambda receives the event, AWS returns OK to the caller. Lambda processes it in the background. Automatic retry on error (up to 2x). Configure a DLQ for permanent failures.
Polling (event source mapping)
SQS, DynamoDB Streams, Kinesis. The Lambda Service polls internally and invokes your function in batches. You don't write a loop — AWS does it for you. This is the mode we'll use with the Inventory consumer.
Partial events (partial batch)
When Lambda receives a batch of 10 SQS messages and 1 fails, without care all of them go back to the queue — including the 9 already processed. Idempotency saves the day, but there's also the ReportBatchItemFailures feature: you return only the IDs that failed, and Lambda returns just those to the queue.
return {
batchItemFailures: [
{ itemIdentifier: "messageId-da-que-falhou-1" },
{ itemIdentifier: "messageId-da-que-falhou-2" }
]
};04. Execution models
Understanding how Lambda executes at runtime is essential for writing performant code.
The execution context concept
When Lambda invokes your function, it creates (or reuses) an execution context — a lightweight container with the runtime, dependencies, and variables. After the execution, the context is kept for some time (a few minutes), ready to be reused.
Cold start vs warm start
| Type | What happens | Latency |
|---|---|---|
| Cold start | New container, code loaded, init runs | ~100-1000ms |
| Warm start | Container reused, only the handler runs | ~1-10ms |
Practical implication
Code outside the handler runs once per context (cold start). Code inside the handler runs on every invocation. This changes everything:
// OUTSIDE the handler — runs only on cold start
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({ region: 'us-east-1' });
// INSIDE the handler — runs on every invocation
export const handler = async (event) => {
// use the client created on cold start
const result = await client.send(...);
return result;
};Concurrency
Lambda scales by creating multiple execution context instances in parallel. Each instance processes one execution at a time.
By default, you get 1000 concurrent executions per region. To limit or reserve capacity, configure reserved concurrency.
05. Cold start and optimization
Cold starts are Lambda's Achilles' heel. In latency-critical APIs, they can be a deal breaker. In async workers, they usually don't matter. But there are techniques to reduce them.
What affects cold start
- Package size — more code, more time to download/load.
- Runtime — Node.js and Python are fast; Java can take seconds.
- Memory — more memory = more CPU = faster startup.
- VPC — functions in a VPC used to have larger cold starts (fixed in 2019).
- Layers — large layers add cold start time.
Mitigation strategies
1. Reduce the package
Don't include what isn't necessary. Use bundlers like esbuild for tree-shaking. Consider ESM instead of CommonJS.
2. Increase memory
A function with 512 MB has about 2x more CPU than one with 256 MB. Cold start drops proportionally. Cost goes up — but if you can cut execution time in half, the total cost may actually decrease.
3. Lazy loading
Instead of importing all deps at the top, import them only when needed. Useful for large deps used in rare paths.
4. Provisioned Concurrency
You pay to keep N instances always warm. This eliminates cold starts for that N. Useful in critical APIs with a required low latency. It costs money even with no invocations.
06. Memory, timeout, and cost
The three settings that most affect cost and performance.
Memory (and implicit CPU)
You choose between 128 MB and 10,240 MB. CPU is proportional: 1,769 MB = 1 full vCPU. More memory = more CPU = faster execution.
Don't think about memory only in terms of what your function consumes — also think about performance. A CPU-bound function with 128 MB can be 3x more expensive than with 1024 MB because it takes 3x longer.
Timeout
The maximum time your function can run before being interrupted. Default: 3 seconds. Maximum: 15 minutes. Configure it based on the real use case.
| Use case | Suggested timeout |
|---|---|
| Simple API (DB query) | 3-5s |
| Process an SQS event | 30s-1min |
| Image conversion | 1-3min |
| Small ETL | 5-15min |
Setting a very high timeout costs nothing — but it increases how long a problematic function keeps running before it dies. Configure what's needed, not the maximum.
How Lambda charges
You pay for:
- Requests: US$ 0.20 per million.
- Compute time: US$ 0.0000166667 per GB-second (memory * time). For example: a function with 256 MB running 100ms, invoked 1 million times:
Compute = 0.25 GB * 0.1s * 1,000,000 = 25,000 GB-seconds
Compute cost = 25,000 * US$ 0.0000166667 = US$ 0.42
Request cost = 1M * US$ 0.20/M = US$ 0.20
Total = US$ 0.62
If <= 1M req and <= 400,000 GB-s per month: FREE (Free Tier)
07. Permissions and IAM
Like Beanstalk, Lambda assumes an IAM role and inherits its permissions. Each function, its own role, with least privilege.
Execution role
Every Lambda needs an execution role — the IAM role it assumes during execution. It defines what your function can do:
- AWSLambdaBasicExecutionRole — write logs to CloudWatch (always include it).
- AWSLambdaSQSQueueExecutionRole — for SQS consumer functions.
- AWSLambdaDynamoDBExecutionRole — for DynamoDB Streams.
- Custom policies — specific permissions for your table, bucket, etc.
Resource-based policies
Some integrations use a policy on the Lambda itself (instead of on the trigger). For example: for S3 to invoke your Lambda, S3 needs a permission registered on the function.
CloudFormation handles this automatically when you define a trigger via Events in AWS::Lambda::Function. Manually, you use aws lambda add-permission.
Pattern for the Inventory consumer
InventoryConsumerRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole
Policies:
- PolicyName: dynamodb-inventory-write
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:UpdateItem
Resource: !ImportValue inventory-db-TableArn08. Migrating the Inventory consumer
In Module 05, the Inventory Service ran on Beanstalk polling SQS. Let's refactor: the HTTP service stays on Beanstalk, but the consumer becomes a separate Lambda.
The new architecture
- It's not a "small server" — it's a different model, with its own characteristics.
- Stateless — each execution is independent; there's no state between calls.
- Time-limited — a maximum of 15 minutes per execution.
- Size-limited — up to 250 MB of code (with layers).
- Cold starts — the first execution has additional latency.
- Managed concurrency — AWS scales automatically up to configured limits.
Function structure
// process-order-event/index.js
// Structure — you write the logic
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, UpdateCommand } from
'@aws-sdk/lib-dynamodb';
const ddb = DynamoDBDocumentClient.from(
new DynamoDBClient({ region: process.env.AWS_REGION })
);
export const handler = async (event) => {
const failures = [];
for (const record of event.Records) {
try {
const eventBody = JSON.parse(record.body);
// Idempotent logic (check if the event was already processed,
// then decrement stock)
await processOrderEvent(eventBody);
} catch (err) {
console.error('Failure:', err);
failures.push({ itemIdentifier: record.messageId });
}
}
return { batchItemFailures: failures };
};Important settings
- Memory: 256 MB (enough for the operation).
- Timeout: 30s (margin for the batch).
- Batch size: 10 (SQS maximum, default).
- Maximum batching window: 5s (acceptable latency vs cost).
- Reserved concurrency: 10 (protect DynamoDB from spikes).
09. Additional project functions
Let's take advantage of Lambda to implement features that are always missing in real systems: notifications, reports, image processing.
send-order-confirmation-email
- Trigger: the email SQS queue (subscribed to the order.created topic).
- Does: sends a confirmation email via SES for the order.
- Memory: 256 MB. Timeout: 10s.
- Permissions: ses:SendEmail.
daily-sales-report
- Trigger: EventBridge schedule (daily cron at 6:00).
- Does: aggregates the previous day's sales, generates a CSV, saves it to S3.
- Memory: 1024 MB. Timeout: 5min.
- Permissions: dynamodb:Query (Orders), s3:PutObject.
resize-product-image
- Trigger: S3 upload to the products-original bucket.
- Does: resizes into 3 sizes (thumb, medium, large) and saves them to products-resized.
- Memory: 1024 MB. Timeout: 1min.
- Layer: sharp (image processing lib).
payment-retry-handler
- Trigger: the payment.failed SNS topic.
- Does: schedules a new attempt in an SQS delay queue, with exponential backoff.
- Memory: 128 MB. Timeout: 5s.
10. Layers and dependencies
Layers are code packages shared across functions. Useful for large deps, common libs, and custom runtimes.
The problem without layers
Each function has its own zip package. If 5 functions use sharp (40 MB), you have 200 MB duplicated. Worse: uploading each function gets slow.
How they work
You create a zip with nodejs/node_modules/<libs> and publish it as a layer. Each function can attach up to 5 layers. The libs appear as if they were in the function's node_modules.
Limitations
- Total size (function + layers): 250 MB uncompressed.
- Maximum of 5 layers per function.
- Layers are per region — you publish in each region where you use them.
- Versioned — every update creates a new version; functions pin to a version.
When to use them
- Large libs shared across functions (sharp, pandas, etc).
- Your project's common code (types, utils, clients).
- Custom runtimes (e.g. Rust, Bash).
- Specific SDKs not included in the default runtime.
11. CloudFormation templates
Lambda in CloudFormation has several particularities. Let's cover the essentials.
Basic Lambda function
InventoryConsumerFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: inventory-consumer
Runtime: nodejs20.x
Handler: index.handler
MemorySize: 256
Timeout: 30
Role: !GetAtt InventoryConsumerRole.Arn
Code:
S3Bucket: !Ref CodeBucket
S3Key: lambdas/inventory-consumer-v1.zip
Environment:
Variables:
AWS_REGION_NAME: !Ref AWS::Region
INVENTORY_TABLE: !ImportValue inventory-db-TableName
ReservedConcurrentExecutions: 10
Tags:
- Key: Project
Value: order-systemEvent source mapping (SQS → Lambda)
InventorySQSEventSource:
Type: AWS::Lambda::EventSourceMapping
Properties:
EventSourceArn: !ImportValue messaging-InventoryQueueArn
FunctionName: !Ref InventoryConsumerFunction
BatchSize: 10
MaximumBatchingWindowInSeconds: 5
FunctionResponseTypes:
- ReportBatchItemFailuresSchedule (EventBridge)
DailyReportSchedule:
Type: AWS::Events::Rule
Properties:
ScheduleExpression: cron(0 6 * * ? *) # 6h UTC every day
Targets:
- Arn: !GetAtt DailyReportFunction.Arn
Id: DailyReport
DailyReportPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref DailyReportFunction
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt DailyReportSchedule.ArnS3 trigger
ProductsBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: products-original-${AWS::AccountId}
NotificationConfiguration:
LambdaConfigurations:
- Event: s3:ObjectCreated:*
Function: !GetAtt ResizeImageFunction.Arn12. Practice exercises
Implement and deploy the Inventory Consumer
Refactor the Module 05 consumer as a Lambda function inventory-consumer. Upload the zip to S3 and use a CloudFormation template. Configure the SQS event source. Test: creating an order in Orders should trigger the Lambda and decrement stock.
Shut down the Beanstalk consumer
Once the Lambda is working, terminate the Beanstalk environment that ran the consumer. Confirm that: (a) the environment was removed; (b) new orders are still being processed (now by the Lambda); (c) the queue isn't piling up.
Implement send-order-confirmation-email
Create the function, configure SES to send from your email (you need to verify a domain/email in the SES sandbox first). Create a subscription on the order-events topic. Confirm: when creating an order, you receive an email.
Implement daily-sales-report
Create the function with an EventBridge schedule (use a test schedule, e.g. every 5 minutes, and later change it to once/day). The function queries the day's Orders via the by-status GSI, generates a CSV in memory, and does a PutObject to S3. Verify that the file appears in the bucket after the cron runs.
Compare costs: before vs after
Written reflection: describe the costs of the Inventory consumer before (running 24/7 on a t2.micro) and after (Lambda invoked only when there are messages). Consider EC2 costs, Lambda costs (free tier!), and maintenance costs (patching, monitoring). At what volume does Beanstalk become cheaper again?
13. Next steps
With Lambda in play, your system combines three compute models: PaaS (Beanstalk) for HTTP services, FaaS (Lambda) for event-driven tasks, and IaC (CloudFormation) tying it all together. One final piece is missing: knowing what's happening in production.
What you learned
- The serverless compute model.
- The anatomy of a function: handler, runtime, configuration.
- The main triggers: API Gateway, SQS, SNS, S3, EventBridge.
- The invocation models: synchronous, asynchronous, polling.
- Cold start, warm start, and optimization strategies.
- How Lambda charges and how to think about cost.
- IAM permissions and the principle of least privilege.
- Migrating the Inventory consumer to Lambda.
- Additional functions: emails, reports, image resize.
- Layers for shared deps.
- Complete CloudFormation templates for Lambda.
What's coming in Module 07
Observability and wrap-up. Now that everything is running, we'll make sure you know what's happening: CloudWatch Logs, metrics, alarms, and X-Ray for distributed tracing. And we'll close out the course with a discussion of next steps: how to evolve the system, and what to study next.
Serverless doesn't mean responsibility-free. Enjoy the journey — Module 07 is next.
