Skip to content

Portal | Level: L2: Operations | Topics: AWS Lambda, Cloud Deep Dive | Domain: Cloud

AWS Lambda - Primer

Why This Matters

Lambda is the core of AWS's serverless compute. When it fits the use case, it eliminates entire categories of operational work — no patching, no scaling decisions, no capacity planning. But Lambda has sharp edges. Cold starts, concurrency limits, timeout traps, and VPC networking quirks bite teams that do not understand the execution model. Knowing when Lambda is the right tool (and when it is not) separates thoughtful architecture from resume-driven development.

Core Concepts

1. Event-Driven Execution Model

Timeline: AWS Lambda launched at re:Invent 2014, initially supporting only Node.js with a 60-second timeout. It was the first major cloud provider to offer Functions-as-a-Service (FaaS). The 15-minute timeout limit arrived in 2018; container image support in 2020; SnapStart (for Java cold start reduction) in 2022.

Lambda runs code in response to events. You do not manage servers. AWS handles provisioning, scaling, and teardown.

Event source  Lambda invocation  Your handler function  Response

Common event sources:
- API Gateway (HTTP requests)
- S3 (object created/deleted)
- SQS (message available)
- DynamoDB Streams (table changes)
- EventBridge (scheduled events, custom events)
- SNS (notifications)
- Kinesis (stream records)
- CloudWatch Logs (log processing)
- ALB (HTTP via load balancer)

You pay only for execution time, billed per millisecond. No charge when the function is idle.

2. Handler Function Anatomy

Every Lambda function has a handler — the entry point AWS calls.

Python:

import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handler(event, context):
    """
    event:   dict with trigger-specific payload
    context: runtime info (request ID, remaining time, memory, etc.)
    """
    logger.info(f"Request ID: {context.aws_request_id}")
    logger.info(f"Time remaining: {context.get_remaining_time_in_millis()}ms")

    # Process the event
    records = event.get("Records", [])
    for record in records:
        body = json.loads(record["body"])
        process_message(body)

    return {
        "statusCode": 200,
        "body": json.dumps({"processed": len(records)})
    }

Node.js:

exports.handler = async (event, context) => {
    console.log(`Request ID: ${context.awsRequestId}`);
    console.log(`Event: ${JSON.stringify(event)}`);

    const response = {
        statusCode: 200,
        body: JSON.stringify({ message: "OK" }),
    };
    return response;
};

The context object provides: - aws_request_id — unique ID for this invocation - function_name, function_version, memory_limit_in_mb - get_remaining_time_in_millis() — how much time you have left - log_group_name, log_stream_name — CloudWatch Logs location

3. Invocation Types

Lambda supports three invocation patterns:

Synchronous (RequestResponse): caller waits for the result. API Gateway, ALB, SDK direct invoke.

aws lambda invoke \
  --function-name my-function \
  --payload '{"key": "value"}' \
  --cli-binary-format raw-in-base64-out \
  response.json

Asynchronous (Event): Lambda queues the event and returns immediately. S3, SNS, EventBridge, CloudFormation. Lambda handles retries (2 retries by default).

aws lambda invoke \
  --function-name my-function \
  --invocation-type Event \
  --payload '{"key": "value"}' \
  --cli-binary-format raw-in-base64-out \
  response.json
# Returns 202 immediately

Event source mapping: Lambda polls the source (SQS, Kinesis, DynamoDB Streams, Kafka) and invokes your function with batches of records.

aws lambda create-event-source-mapping \
  --function-name my-function \
  --event-source-arn arn:aws:sqs:us-east-1:123456789012:my-queue \
  --batch-size 10 \
  --maximum-batching-window-in-seconds 5

4. Cold Starts and Warm Starts

When Lambda creates a new execution environment, it must download your code, initialize the runtime, and run your initialization code. This is a cold start.

Cold start timeline:
┌─────────────────────────────────────────────────────────────────┐
│ Download code │ Init runtime │ Init your code │ Handle request  │
│   ~50-200ms   │   ~50-100ms  │   varies       │   your code     │
└─────────────────────────────────────────────────────────────────┘
                 ← cold start overhead →

Warm start:
┌──────────────┐
│ Handle request│  (environment already exists)
└──────────────┘

Cold start factors: - Memory size: more memory = faster init (proportional CPU) - Package size: larger deployments = longer download - Runtime: Python and Node.js start faster than Java and .NET - VPC: adds ENI attachment time (greatly improved since 2019, but still measurable) - Init code: imports, DB connections, SDK clients — put these outside the handler

# GOOD: initialize outside handler (runs once per cold start)
import boto3
import os

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])

def handler(event, context):
    # table is already initialized on warm invocations
    item = table.get_item(Key={"id": event["id"]})
    return item

5. Execution Environment Lifecycle

INIT phase:
  ├── Extension init (if any)
  ├── Runtime init
  └── Function init (your top-level code)

INVOKE phase (repeats for warm invocations):
  └── Handler called with event + context

SHUTDOWN phase (when environment is recycled):
  ├── Runtime shutdown hook
  └── Extension shutdown hook

AWS may freeze the execution environment between invocations. Your function should not depend on background processes, open file handles, or in-memory state persisting between invocations. It usually does persist (warm start), but it is not guaranteed.

6. Lambda Layers

Layers let you share code, libraries, and data across multiple functions. Each function can use up to 5 layers.

# Create a layer from a zip (must follow directory structure)
# python/lib/python3.11/site-packages/<your-packages>
cd /tmp && mkdir -p python/lib/python3.11/site-packages
pip install requests -t python/lib/python3.11/site-packages/
zip -r requests-layer.zip python/

aws lambda publish-layer-version \
  --layer-name requests \
  --zip-file fileb://requests-layer.zip \
  --compatible-runtimes python3.11 python3.12

# Attach layer to function
aws lambda update-function-configuration \
  --function-name my-function \
  --layers arn:aws:lambda:us-east-1:123456789012:layer:requests:1

Common layers: AWS SDK updates, shared utilities, monitoring agents (Datadog, New Relic), Lambda Powertools.

7. Environment Variables and Configuration

aws lambda update-function-configuration \
  --function-name my-function \
  --environment '{
    "Variables": {
      "TABLE_NAME": "users",
      "LOG_LEVEL": "INFO",
      "STAGE": "production"
    }
  }'

For secrets, use AWS Secrets Manager or SSM Parameter Store — not environment variables:

import boto3
import json

# Cache the secret outside the handler
secrets_client = boto3.client("secretsmanager")
_cached_secret = None

def get_db_password():
    global _cached_secret
    if _cached_secret is None:
        response = secrets_client.get_secret_value(SecretId="prod/db/password")
        _cached_secret = json.loads(response["SecretString"])
    return _cached_secret

def handler(event, context):
    creds = get_db_password()
    # Use creds["password"] to connect

8. IAM Execution Role

Every Lambda function has an execution role that defines what AWS services it can access.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/users"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:us-east-1:123456789012:*"
    }
  ]
}

The trust policy must allow Lambda to assume the role:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "lambda.amazonaws.com"},
    "Action": "sts:AssumeRole"
  }]
}

9. VPC-Attached Lambda

By default, Lambda runs in an AWS-managed VPC with internet access. When you attach Lambda to your VPC, it can access private resources (RDS, ElastiCache, internal APIs) but loses internet access unless you provide a NAT Gateway or VPC endpoint.

aws lambda update-function-configuration \
  --function-name my-function \
  --vpc-config '{
    "SubnetIds": ["subnet-priv1a", "subnet-priv1b"],
    "SecurityGroupIds": ["sg-lambda"]
  }'

The VPC Lambda networking model (post-2019): - Lambda creates Hyperplane ENIs in your subnets during function creation/update - Cold start no longer includes ENI creation (the old 10-15 second penalty is gone) - You still need a NAT Gateway for outbound internet access - Use VPC endpoints for AWS service access without NAT (cheaper)

10. Concurrency

Every active invocation consumes one unit of concurrency. Account default: 1,000 concurrent executions across all functions.

Unreserved concurrency: shared pool. All functions compete. One runaway function can starve others.

Reserved concurrency: guarantees capacity for a function AND caps it. A function with reserved=100 will never exceed 100 concurrent invocations, and will always have 100 available.

Provisioned concurrency: pre-warms execution environments. Eliminates cold starts. You pay for the provisioned capacity whether it is used or not.

# Set reserved concurrency (cap + guarantee)
aws lambda put-function-concurrency \
  --function-name my-function \
  --reserved-concurrent-executions 100

# Set provisioned concurrency (pre-warm for zero cold starts)
aws lambda put-provisioned-concurrency-config \
  --function-name my-function \
  --qualifier prod \
  --provisioned-concurrent-executions 50

# Check current concurrency usage
aws lambda get-account-settings \
  --query '{Total:AccountLimit.TotalCodeSize,Concurrency:AccountLimit.ConcurrentExecutions}'

11. Destinations and Dead Letter Queues

For async invocations, configure where successful and failed invocations go:

# Destinations (preferred over DLQ — more flexible)
aws lambda put-function-event-invoke-config \
  --function-name my-function \
  --destination-config '{
    "OnSuccess": {"Destination": "arn:aws:sqs:...:success-queue"},
    "OnFailure": {"Destination": "arn:aws:sqs:...:failure-queue"}
  }' \
  --maximum-retry-attempts 2 \
  --maximum-event-age-in-seconds 3600

Dead letter queues (DLQ) receive events that fail all retries:

aws lambda update-function-configuration \
  --function-name my-function \
  --dead-letter-config '{
    "TargetArn": "arn:aws:sqs:us-east-1:123456789012:my-dlq"
  }'

12. Memory, CPU, and Timeout

Lambda allocates CPU proportional to memory. More memory = more CPU = faster execution = potentially cheaper (shorter duration).

128 MB  → minimal CPU (cheapest per-ms, slowest)
1769 MB → 1 full vCPU
3538 MB → 2 vCPUs (multi-threaded code benefits)
10240 MB → 6 vCPUs (maximum)

Remember: The magic number is 1769 MB = 1 vCPU. Below that, you get a fraction of a CPU. A function at 128 MB gets roughly 1/14th of a vCPU. Doubling memory doubles CPU and often halves execution time — making the cost roughly the same but the user experience much better.

# Configure memory and timeout
aws lambda update-function-configuration \
  --function-name my-function \
  --memory-size 1024 \
  --timeout 30

# Maximum timeout: 900 seconds (15 minutes)
# /tmp storage: 512 MB (default), up to 10,240 MB

13. Lambda@Edge vs CloudFront Functions

Both run code at CloudFront edge locations but serve different purposes:

Feature Lambda@Edge CloudFront Functions
Runtime Node.js, Python JavaScript only
Execution time Up to 30s (origin) / 5s (viewer) Sub-millisecond (< 2ms)
Memory Up to 10 GB 2 MB
Network access Yes No
Use case Complex transforms, auth with DB lookup Header manipulation, URL rewrite, simple auth
Price Per request + duration Per request only (cheaper)

14. Lambda Powertools

AWS Lambda Powertools is the standard library for production Lambda functions. Available for Python, TypeScript, Java, and .NET.

from aws_lambda_powertools import Logger, Tracer, Metrics
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.utilities.typing import LambdaContext

logger = Logger()
tracer = Tracer()
metrics = Metrics()
app = APIGatewayRestResolver()

@app.get("/users/<user_id>")
@tracer.capture_method
def get_user(user_id: str):
    logger.info(f"Fetching user {user_id}")
    metrics.add_metric(name="UserFetched", unit="Count", value=1)
    return {"user_id": user_id, "name": "Alice"}

@logger.inject_lambda_context
@tracer.capture_lambda_handler
@metrics.log_metrics
def handler(event: dict, context: LambdaContext):
    return app.resolve(event, context)

Powertools provides: structured logging, distributed tracing (X-Ray), custom metrics, idempotency, feature flags, event parsing, batch processing, and more.

15. Deployment Patterns

Direct deploy (CLI/SDK):

zip function.zip handler.py
aws lambda update-function-code \
  --function-name my-function \
  --zip-file fileb://function.zip

SAM (Serverless Application Model):

# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: handler.handler
      Runtime: python3.12
      MemorySize: 256
      Timeout: 30
      Events:
        Api:
          Type: Api
          Properties:
            Path: /users/{id}
            Method: get

sam build && sam deploy --guided

CDK and Terraform are also common. The choice depends on your team's IaC standard.

Key Takeaways

  • Lambda scales automatically but concurrency is shared by default — set reserved concurrency for critical functions
  • Cold starts are real — initialize SDK clients and DB connections outside the handler
  • More memory = more CPU = often cheaper total cost (shorter duration offsets higher per-ms price)
  • VPC Lambda loses internet access without a NAT Gateway — use VPC endpoints where possible
  • Async invocations retry twice by default — use destinations or DLQ to capture failures
  • Lambda Powertools is the standard library for production functions — do not reinvent logging/tracing

    Default trap: API Gateway has a hard 29-second integration timeout that cannot be increased. If your Lambda timeout is 30 seconds or more, API Gateway will return 504 before Lambda finishes. Always set Lambda timeout to at least 1 second less than the upstream timeout.

  • Timeout must be shorter than upstream caller's timeout (API Gateway has a hard 29-second limit)

  • /tmp storage is shared across warm invocations but not guaranteed to persist

Wiki Navigation

Prerequisites

  • AWS CloudWatch (Topic Pack, L2) — Cloud Deep Dive
  • AWS Devops Flashcards (CLI) (flashcard_deck, L1) — Cloud Deep Dive
  • AWS EC2 (Topic Pack, L1) — Cloud Deep Dive
  • AWS ECS (Topic Pack, L2) — Cloud Deep Dive
  • AWS General Flashcards (CLI) (flashcard_deck, L1) — Cloud Deep Dive
  • AWS IAM (Topic Pack, L1) — Cloud Deep Dive
  • AWS Networking (Topic Pack, L1) — Cloud Deep Dive
  • AWS Route 53 (Topic Pack, L2) — Cloud Deep Dive
  • AWS S3 Deep Dive (Topic Pack, L1) — Cloud Deep Dive
  • Azure Flashcards (CLI) (flashcard_deck, L1) — Cloud Deep Dive