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

  1. Lambda in 5 minutes
  2. Anatomy of a function
  3. Triggers: how Lambda is invoked
  4. Execution models
  5. Cold start and optimization
  6. Memory, timeout, and cost
  7. Permissions and IAM
  8. Migrating the Inventory consumer
  9. Additional project functions
  10. Layers and dependencies
  11. CloudFormation templates
  12. Practice exercises
  13. 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

TriggerWhen to use
API GatewayHTTP/REST/WebSocket APIs
SQSConsume queue messages (replaces polling)
SNSReact to pub/sub notifications
S3Process files: upload, delete, etc
DynamoDB StreamsReact to table changes
EventBridgeSchedule (cron) or custom events
CloudWatch LogsProcess/forward logs
KinesisData 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

TypeWhat happensLatency
Cold startNew container, code loaded, init runs~100-1000ms
Warm startContainer 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 caseSuggested timeout
Simple API (DB query)3-5s
Process an SQS event30s-1min
Image conversion1-3min
Small ETL5-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-TableArn

08. 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-system

Event source mapping (SQS → Lambda)

InventorySQSEventSource:
  Type: AWS::Lambda::EventSourceMapping
  Properties:
    EventSourceArn: !ImportValue messaging-InventoryQueueArn
    FunctionName: !Ref InventoryConsumerFunction
    BatchSize: 10
    MaximumBatchingWindowInSeconds: 5
    FunctionResponseTypes:
      - ReportBatchItemFailures

Schedule (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.Arn

S3 trigger

ProductsBucket:
  Type: AWS::S3::Bucket
  Properties:
    BucketName: products-original-${AWS::AccountId}
    NotificationConfiguration:
      LambdaConfigurations:
        - Event: s3:ObjectCreated:*
          Function: !GetAtt ResizeImageFunction.Arn

12. Practice exercises

Exercício
01

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.

Exercício
02

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.

Exercício
03

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.

Exercício
04

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.

Exercício
05

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.