All topics
Cloud · Learning hub

Lambda notes for developers

Master Lambda with a curated set of 8 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Cloud notes
Lambda

Lambda Fundamentals

Lambda Fundamentals AWS Lambda runs code in response to events without managing servers. Understanding the handler signature, context object, and execution envi

Lambda Fundamentals

AWS Lambda runs code in response to events without managing servers. Understanding the handler signature, context object, and execution environment lifecycle is the foundation for writing reliable Lambda functions.

Handler Signatures

// Node.js 20 — async handler (preferred)
export const handler = async (event, context) => {
  console.log("Event:", JSON.stringify(event, null, 2));
  console.log("Function name:", context.functionName);
  console.log("Remaining time (ms):", context.getRemainingTimeInMillis());

  // Return value becomes the response (for sync invocations like API GW)
  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message: "Hello from Lambda" }),
  };
};

// Node.js — callback style (legacy, avoid for new code)
exports.handler = function(event, context, callback) {
  callback(null, { statusCode: 200, body: "OK" }); // (error, result)
};

// context object key properties:
// context.functionName           — e.g. "my-function"
// context.functionVersion        — "$LATEST" or version number
// context.invokedFunctionArn     — full ARN of the invoked function/alias
// context.awsRequestId           — unique request ID for this invocation
// context.logGroupName           — CloudWatch log group
// context.logStreamName          — CloudWatch log stream (unique per instance)
// context.memoryLimitInMB        — configured memory
// context.getRemainingTimeInMillis() — ms until timeout
# Python 3.12 handler
# def handler(event: dict, context) -> dict:
#     import json
#     print(f"Request ID: {context.aws_request_id}")
#     print(f"Remaining time: {context.get_remaining_time_in_millis()}ms")
#     return {"statusCode": 200, "body": json.dumps({"message": "OK"})}

# Go handler
# package main
# import (
#   "context"
#   "github.com/aws/aws-lambda-go/lambda"
#   "github.com/aws/aws-lambda-go/events"
# )
# func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
#   return events.APIGatewayProxyResponse{StatusCode: 200, Body: "OK"}, nil
# }
# func main() { lambda.Start(handler) }

# Invoke Lambda from CLI
aws lambda invoke   --function-name my-function   --payload '{"key": "value"}'   --cli-binary-format raw-in-base64-out   output.json
cat output.json

# Invoke asynchronously (fire and forget)
aws lambda invoke   --function-name my-function   --invocation-type Event   --payload '{"key": "value"}'   --cli-binary-format raw-in-base64-out   /dev/null

# View CloudWatch logs
aws logs tail /aws/lambda/my-function --follow

Execution Environment & Cold Starts

// Execution environment lifecycle:
// INIT phase:    downloads code, starts runtime, runs initialization code (outside handler)
// INVOKE phase:  runs handler for each invocation (environment reused if warm)
// SHUTDOWN:      environment frozen, not immediately destroyed

// Cold start: new execution environment allocated (INIT + INVOKE)
// Warm start: existing environment reused (INVOKE only) — much faster

// Code OUTSIDE the handler runs only during cold start (initialization)
// Put expensive setup here (DB connections, SDK clients, config loading):
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";

// This runs once per cold start, reused across warm invocations
const ddbClient = new DynamoDBClient({ region: "us-east-1" });
const docClient = DynamoDBDocumentClient.from(ddbClient);

// Cached config (avoid fetching on every invocation)
let config = null;
async function getConfig() {
  if (!config) {
    // Fetch from SSM/Secrets Manager only on cold start or cache miss
    config = await fetchConfigFromSSM();
  }
  return config;
}

export const handler = async (event) => {
  const cfg = await getConfig();   // Uses cached value on warm invocations
  // ... business logic
};

// Lambda Function URL — invoke Lambda directly over HTTPS (no API Gateway)
// Provides a dedicated URL: https://<url-id>.lambda-url.<region>.on.aws
aws lambda create-function-url-config   --function-name my-function   --auth-type AWS_IAM    # or NONE for public endpoints
# Returns: FunctionUrl: https://abc123.lambda-url.us-east-1.on.aws/
Lambda

Event Sources & Triggers

Event Sources & Triggers Lambda integrates with virtually every AWS service. The event payload structure varies by source — knowing the shape of the event objec

Event Sources & Triggers

Lambda integrates with virtually every AWS service. The event payload structure varies by source — knowing the shape of the event object for each trigger is critical for writing correct handler code.

API Gateway & S3

// API Gateway HTTP API (payload format version 2.0) event shape
{
  "version": "2.0",
  "routeKey": "POST /users",
  "rawPath": "/users",
  "rawQueryString": "page=1",
  "headers": { "content-type": "application/json", ... },
  "queryStringParameters": { "page": "1" },
  "pathParameters": { "userId": "123" },
  "body": "{"name": "Alice"}",  // string, not parsed
  "isBase64Encoded": false,
  "requestContext": {
    "accountId": "123456789012",
    "http": { "method": "POST", "path": "/users", "sourceIp": "1.2.3.4" },
    "requestId": "abc-123"
  }
}

// Handler parsing API GW HTTP API event
export const handler = async (event) => {
  const body = JSON.parse(event.body || "{}");
  const userId = event.pathParameters?.userId;
  const page = parseInt(event.queryStringParameters?.page || "1", 10);
  // ...
  return { statusCode: 201, body: JSON.stringify({ id: "new-user-id" }) };
};

// S3 event — triggered on object create/delete
{
  "Records": [{
    "eventSource": "aws:s3",
    "eventName": "ObjectCreated:Put",
    "s3": {
      "bucket": { "name": "my-bucket" },
      "object": {
        "key": "uploads/image.png",   // URL-encoded — decode before use
        "size": 102400
      }
    }
  }]
}

// S3 handler
export const handler = async (event) => {
  for (const record of event.Records) {
    const bucket = record.s3.bucket.name;
    const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));
    console.log(`Processing s3://${bucket}/${key}`);
    // process image, extract metadata, etc.
  }
};

SQS, SNS & DynamoDB Streams

// SQS event — batch of messages (up to 10 by default)
{
  "Records": [{
    "messageId": "abc-123",
    "body": "{"orderId": "order-456"}",   // string, must be parsed
    "attributes": {
      "ApproximateReceiveCount": "1",
      "SentTimestamp": "1703001600000"
    },
    "receiptHandle": "AQEBxxx..."    // Used to delete message (Lambda does this automatically on success)
  }]
}

// SQS handler with partial batch failure reporting
export const handler = async (event) => {
  const failures = [];
  for (const record of event.Records) {
    try {
      const payload = JSON.parse(record.body);
      await processOrder(payload.orderId);
    } catch (err) {
      console.error("Failed:", record.messageId, err);
      failures.push({ itemIdentifier: record.messageId });
    }
  }
  // Return failed message IDs — Lambda will requeue only these
  return { batchItemFailures: failures };
};
// Enable "Report batch item failures" in the SQS event source mapping

// SNS event — Lambda subscribed to a topic
{
  "Records": [{
    "EventSource": "aws:sns",
    "Sns": {
      "Message": "{"type": "user.created", "userId": "123"}",
      "Subject": "User Event",
      "TopicArn": "arn:aws:sns:us-east-1:123:my-topic"
    }
  }]
}

// DynamoDB Streams — process table changes
{
  "Records": [{
    "eventSource": "aws:dynamodb",
    "eventName": "INSERT",   // INSERT | MODIFY | REMOVE
    "dynamodb": {
      "Keys": { "pk": { "S": "USER#123" } },
      "NewImage": { "pk": { "S": "USER#123" }, "name": { "S": "Alice" } },
      "OldImage": null   // null for INSERT
    }
  }]
}

EventBridge & Step Functions

# EventBridge — serverless event bus
# Schedule a Lambda every 5 minutes (cron expression)
aws events put-rule   --name "run-every-5-min"   --schedule-expression "rate(5 minutes)"   --state ENABLED

aws lambda add-permission   --function-name my-function   --statement-id EventBridgeInvoke   --action lambda:InvokeFunction   --principal events.amazonaws.com   --source-arn arn:aws:events:us-east-1:123:rule/run-every-5-min

aws events put-targets   --rule "run-every-5-min"   --targets "Id=1,Arn=arn:aws:lambda:us-east-1:123:function:my-function"

# EventBridge event payload
# { "version": "0", "id": "...", "source": "aws.ec2", "detail-type": "EC2 Instance State-change Notification",
#   "detail": { "instance-id": "i-123", "state": "running" } }

# Step Functions — orchestrate Lambda functions
# Define a state machine in JSON/YAML (ASL — Amazon States Language)
# Lambda invoked as a Task state, receives input, returns output to next state
# Supports parallel execution, error handling, wait states, and human approval steps

# Kinesis Data Streams — real-time streaming
# Lambda reads batches of records from a Kinesis shard
# Records contain base64-encoded data
# export const handler = async (event) => {
#   for (const record of event.Records) {
#     const data = Buffer.from(record.kinesis.data, "base64").toString("utf-8");
#     const payload = JSON.parse(data);
#     await processMetric(payload);
#   }
# };
Lambda

Deployment & Configuration

Lambda Deployment & Configuration Lambda can be deployed as a .zip package or a container image. Layers enable code sharing. Versions and aliases enable safe pr

Lambda Deployment & Configuration

Lambda can be deployed as a .zip package or a container image. Layers enable code sharing. Versions and aliases enable safe production deployments. AWS SAM and the Serverless Framework simplify infrastructure management.

Deployment Packages & Layers

# Zip deployment package — Node.js
npm ci --production
zip -r function.zip index.js node_modules/
aws lambda update-function-code   --function-name my-function   --zip-file fileb://function.zip

# Deploy from S3 (preferred for large packages > 50 MB)
aws s3 cp function.zip s3://my-deploy-bucket/function.zip
aws lambda update-function-code   --function-name my-function   --s3-bucket my-deploy-bucket   --s3-key function.zip

# Container image deployment (up to 10 GB — for ML models, large dependencies)
# Dockerfile
# FROM public.ecr.aws/lambda/nodejs:20
# COPY index.js package*.json ./
# RUN npm ci --production
# CMD ["index.handler"]

# Build and push to ECR
aws ecr create-repository --repository-name my-lambda-repo
aws ecr get-login-password | docker login --username AWS --password-stdin <account>.dkr.ecr.us-east-1.amazonaws.com
docker build -t my-lambda-repo .
docker tag my-lambda-repo:latest <account>.dkr.ecr.us-east-1.amazonaws.com/my-lambda-repo:latest
docker push <account>.dkr.ecr.us-east-1.amazonaws.com/my-lambda-repo:latest

aws lambda update-function-code   --function-name my-function   --image-uri <account>.dkr.ecr.us-east-1.amazonaws.com/my-lambda-repo:latest

# Lambda Layers — shared code/dependencies across functions (up to 5 layers)
# Create a layer
mkdir -p nodejs/node_modules && npm install --prefix nodejs sharp
zip -r sharp-layer.zip nodejs/
aws lambda publish-layer-version   --layer-name sharp-image-layer   --zip-file fileb://sharp-layer.zip   --compatible-runtimes nodejs20.x

# Attach layer to function
aws lambda update-function-configuration   --function-name my-function   --layers arn:aws:lambda:us-east-1:123:layer:sharp-image-layer:1

Configuration & Concurrency

# Memory (128 MB – 10 GB) — also controls CPU allocation proportionally
# Timeout (1 sec – 15 min)
aws lambda update-function-configuration   --function-name my-function   --memory-size 1024   --timeout 30

# Environment variables (not for secrets — use Secrets Manager or SSM)
aws lambda update-function-configuration   --function-name my-function   --environment "Variables={NODE_ENV=production,LOG_LEVEL=info,TABLE_NAME=users-prod}"

# Concurrency:
# Default: unreserved (all functions share account limit of 1000)
# Reserved concurrency: guarantee N concurrent executions for a function (also acts as throttle)
# Provisioned concurrency: pre-warm N execution environments (eliminates cold starts)

# Set reserved concurrency
aws lambda put-function-concurrency   --function-name my-function   --reserved-concurrent-executions 50

# Provisioned concurrency (on an alias or version)
aws lambda put-provisioned-concurrency-config   --function-name my-function   --qualifier prod   --provisioned-concurrent-executions 10

# Versions & Aliases
# Publish a version (immutable snapshot of current $LATEST)
aws lambda publish-version --function-name my-function

# Create alias pointing to version (stable ARN for callers)
aws lambda create-alias   --function-name my-function   --name prod   --function-version 5

# Weighted alias for canary deployment (10% to new version, 90% to old)
aws lambda update-alias   --function-name my-function   --name prod   --routing-config "AdditionalVersionWeights={6=0.1}"  # version 6 gets 10%

AWS SAM & Serverless Framework

# AWS SAM (Serverless Application Model) — template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Runtime: nodejs20.x
    Timeout: 30
    MemorySize: 512
    Environment:
      Variables:
        TABLE_NAME: !Ref UsersTable

Resources:
  GetUserFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/getUser.handler
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref UsersTable
      Events:
        Api:
          Type: HttpApi
          Properties:
            Path: /users/{userId}
            Method: GET

  CreateUserFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/createUser.handler
      Policies:
        - DynamoDBWritePolicy:
            TableName: !Ref UsersTable
      Events:
        Api:
          Type: HttpApi
          Properties:
            Path: /users
            Method: POST

  UsersTable:
    Type: AWS::DynamoDB::Table
    Properties:
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: pk
          AttributeType: S
      KeySchema:
        - AttributeName: pk
          KeyType: HASH

# SAM CLI commands
# sam build                     — build + package
# sam local invoke GetUserFunction --event event.json
# sam local start-api           — local API GW emulation
# sam deploy --guided           — interactive first deploy
# sam deploy                    — subsequent deploys (uses samconfig.toml)
Lambda

Performance & Best Practices

Lambda Performance & Best Practices Cold starts, connection reuse, error handling, and VPC gotchas are the main operational pain points for Lambda in production

Lambda Performance & Best Practices

Cold starts, connection reuse, error handling, and VPC gotchas are the main operational pain points for Lambda in production. Addressing these systematically leads to faster, cheaper, and more reliable serverless functions.

Cold Start Optimization

// Cold start contributors (in order of impact):
// 1. Deployment package size — trim dependencies aggressively
// 2. Runtime — Node.js/Python faster than Java/Kotlin
// 3. VPC attachment — adds 100-500ms for ENI setup (use Lambda SnapStart for Java)
// 4. Memory — higher memory = more CPU = faster init
// 5. SDK imports — use modular @aws-sdk/client-* not the monolithic aws-sdk v2

// BAD — imports entire AWS SDK (~40 MB in v2)
// const AWS = require("aws-sdk");
// const s3 = new AWS.S3();

// GOOD — import only the clients you need (tree-shakeable)
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";

// Minimize dependencies — use npm analyze
// npx webpack-bundle-analyzer stats.json
// npx esbuild src/index.ts --bundle --platform=node --minify --outfile=dist/index.js

// Use esbuild for ultra-fast bundling (smaller output than webpack)
// package.json build script:
// "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --outfile=dist/index.js"

// Provisioned Concurrency (eliminates cold starts for latency-sensitive functions)
// Applied to an alias or version — pre-warms N execution environments
// Cost: charged per provisioned-concurrency-hours regardless of invocations
// Use Lambda Power Tuning to find optimal memory/PC configuration:
// https://github.com/alexcasalboni/aws-lambda-power-tuning

// Right-size memory using Lambda Power Tuning State Machine (Step Functions)
// Invoke the state machine with:
// { "lambdaARN": "arn:...", "powerValues": [128,256,512,1024,2048],
//   "num": 50, "payload": {}, "parallelInvocation": true }

Connection Reuse & VPC Gotchas

// Database connection reuse — initialize OUTSIDE the handler
// Connection is reused across warm invocations (same execution environment)
import { Pool } from "pg";

// Created once per cold start, reused across warm invocations
const pool = new Pool({
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 2,             // Keep small — Lambda can have many concurrent instances
  idleTimeoutMillis: 0,
  connectionTimeoutMillis: 2000,
});

export const handler = async (event) => {
  const client = await pool.connect();
  try {
    const result = await client.query("SELECT * FROM users WHERE id = $1", [event.userId]);
    return result.rows[0];
  } finally {
    client.release();
  }
};

// RDS Proxy — use instead of direct DB connections for Lambda
// Pools connections at the proxy level, handles Lambda burst scaling
// Eliminates "too many connections" errors when Lambda scales to hundreds of instances
// Add RDS_PROXY_ENDPOINT to environment variables and connect to it instead

// VPC Lambda gotchas:
// - Adds ~100ms cold start (ENI allocation) — eliminated in 2019 with Hyperplane ENI
//   but VPC DNS resolution can still add latency
// - Lambda in a private subnet needs NAT Gateway for internet access
//   (costs ~$0.045/hr + data processing)
// - Lambda NOT in VPC cannot access RDS/ElastiCache in VPC
// - Use VPC endpoints (interface/gateway) to access AWS services without NAT GW:
//   S3, DynamoDB gateway endpoints are free; SQS, SNS, etc. cost $0.01/hr

Error Handling — DLQ & Destinations

# Retry behavior by invocation type:
# Synchronous (API GW, ALB): no retry — caller gets the error immediately
# Asynchronous (S3, SNS, EventBridge): 2 automatic retries with exponential backoff
# Event source mapping (SQS, Kinesis, DynamoDB Streams): retries until message expires or batch succeeds

# Dead Letter Queue (DLQ) — for async invocations only
# Failed events (after retries) go to SQS queue or SNS topic
aws lambda update-function-configuration   --function-name my-function   --dead-letter-config "TargetArn=arn:aws:sqs:us-east-1:123:my-dlq"

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

# SQS event source: partial batch failure (return failed message IDs)
# Enable "Report batch item failures" on the event source mapping
aws lambda update-event-source-mapping   --uuid <mapping-id>   --function-response-types ReportBatchItemFailures

# Structured logging for easier CloudWatch Insights queries
import { Logger } from "@aws-lambda-powertools/logger";
const logger = new Logger({ serviceName: "user-service", logLevel: "INFO" });

export const handler = async (event) => {
  logger.addContext(context);
  logger.info("Processing request", { userId: event.userId, action: "getUser" });
  // CloudWatch Insights query: fields @timestamp, userId, action | filter level = "INFO"
};

# Lambda Powertools for TypeScript/Python (AWS-maintained utility library)
# Provides: structured logging, tracing (X-Ray), metrics (CloudWatch EMF), parameters, batch
npm install @aws-lambda-powertools/logger @aws-lambda-powertools/tracer @aws-lambda-powertools/metrics
Lambda

Fundamentals & Execution Model

AWS Lambda: Fundamentals & Execution Model Lambda is AWS's serverless compute service. You provide code; AWS runs it in response to events, manages scaling, and

AWS Lambda: Fundamentals & Execution Model

Lambda is AWS's serverless compute service. You provide code; AWS runs it in response to events, manages scaling, and charges only for compute time used. No servers to provision, no idle costs.

Function Anatomy

// Node.js handler signature
export const handler = async (event, context) => {
  // event    — input from the trigger (API GW request, S3 event, SQS message, etc.)
  // context  — runtime info (requestId, functionName, remainingTimeInMillis, etc.)

  console.log('Event:', JSON.stringify(event, null, 2));
  console.log('Remaining time:', context.getRemainingTimeInMillis(), 'ms');

  // Return value: for sync invocations (API GW), this becomes the response
  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message: 'Hello!' }),
  };
};
# Python handler
import json

def handler(event, context):
    print(f"Request ID: {context.aws_request_id}")
    print(f"Remaining time: {context.get_remaining_time_in_millis()}ms")

    return {
        'statusCode': 200,
        'body': json.dumps({'message': 'Hello!'})
    }

Supported Runtimes

  • Node.js 22.x, 20.x, 18.x (most popular for web APIs)

  • Python 3.13, 3.12, 3.11, 3.10 (popular for data/ML/scripting)

  • Java 21, 17, 11, 8 (SnapStart for cold start reduction)

  • Go 1.x (custom runtime, fastest cold starts)

  • Ruby 3.3, 3.2

  • .NET 8 (C#)

  • Custom Runtime: any language via bootstrap binary (Rust, Bash, etc.)

  • Container image: up to 10GB, any language/runtime, ECR-hosted

Execution Environment Lifecycle

Lambda reuses execution environments for subsequent invocations. Code at module level runs once during the INIT phase; handler runs per invocation. Use this to cache DB connections, SDK clients, etc.

// INIT phase — runs once per cold start
// Initialize expensive resources OUTSIDE the handler
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';

// DB client created once, reused across warm invocations
const ddbClient = new DynamoDBClient({});
const db = DynamoDBDocumentClient.from(ddbClient);

// INVOKE phase — runs every time
export const handler = async (event) => {
  // db is already initialized — no cold start penalty here
  const result = await db.get({ TableName: 'users', Key: { id: event.userId } });
  return result.Item;
};

Configuration Limits

Setting              | Default  | Max
---------------------|----------|------------------
Memory               | 128 MB   | 10,240 MB (10 GB)
Timeout              | 3 sec    | 900 sec (15 min)
/tmp storage         | 512 MB   | 10,240 MB
Env variables        | —        | 4 KB total
Payload (sync)       | —        | 6 MB request/response
Payload (async)      | —        | 256 KB
Container image size | —        | 10 GB (uncompressed)
Deployment pkg size  | —        | 50 MB zipped, 250 MB unzipped

Memory & CPU Relationship

Lambda does not let you set CPU directly. More memory = more vCPU proportionally. At 1769 MB you get 1 full vCPU; 3538 MB gives 2 vCPUs. For CPU-bound tasks, increasing memory reduces duration, often making the total cost neutral or cheaper.

# Create function
aws lambda create-function \
  --function-name my-api-handler \
  --runtime nodejs20.x \
  --handler index.handler \
  --role arn:aws:iam::123456789012:role/lambda-exec-role \
  --zip-file fileb://function.zip \
  --memory-size 512 \
  --timeout 30 \
  --environment Variables='{DB_URL=postgresql://...,LOG_LEVEL=info}'

# Update config
aws lambda update-function-configuration \
  --function-name my-api-handler \
  --memory-size 1024 \
  --timeout 60

Lambda Layers

Layers are ZIP archives with libraries, custom runtimes, or configuration. Each function can use up to 5 layers. Shared across functions, versioned, useful for large dependencies (e.g., numpy/pandas for Python).

# Create layer
zip -r layer.zip nodejs/   # Must follow structure: nodejs/node_modules/...
aws lambda publish-layer-version \
  --layer-name my-shared-deps \
  --zip-file fileb://layer.zip \
  --compatible-runtimes nodejs20.x

# Attach layer to function
aws lambda update-function-configuration \
  --function-name my-api-handler \
  --layers arn:aws:lambda:eu-west-1:123:layer:my-shared-deps:3

Environment Variables & Secrets

# Env vars encrypted at rest with KMS (Lambda service key by default)
# For sensitive values: use Secrets Manager or SSM Parameter Store at runtime
aws lambda update-function-configuration \
  --function-name my-api-handler \
  --environment Variables='{
    "NODE_ENV": "production",
    "LOG_LEVEL": "warn"
  }'
// Fetch secret at cold start (cache for warm invocations)
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

const sm = new SecretsManagerClient({});
let dbPassword; // cached across warm invocations

async function getDbPassword() {
  if (dbPassword) return dbPassword;
  const res = await sm.send(new GetSecretValueCommand({ SecretId: 'prod/db/password' }));
  dbPassword = JSON.parse(res.SecretString).password;
  return dbPassword;
}
Lambda

Event Sources & Integrations

Lambda: Event Sources & Integrations Lambda can be triggered synchronously (caller waits for response), asynchronously (fire and forget), or via polling (Lambda

Lambda: Event Sources & Integrations

Lambda can be triggered synchronously (caller waits for response), asynchronously (fire and forget), or via polling (Lambda polls the source).

Invocation Types

  • Synchronous: API Gateway, ALB, Function URL, Cognito, Alexa — caller waits, errors returned to caller

  • Asynchronous: S3, SNS, EventBridge, SES, CloudFormation — event queued, Lambda retries on error (2 times)

  • Poll-based: SQS, Kinesis, DynamoDB Streams, Kafka — Lambda polls, processes batches

API Gateway & Function URLs

// HTTP API (API GW v2) event structure
export const handler = async (event) => {
  const { method, path, queryStringParameters, headers, body } = event;
  const parsedBody = body ? JSON.parse(body) : null;

  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
    },
    body: JSON.stringify({ method, path, data: parsedBody }),
  };
};
# Create Function URL (no API GW needed, simpler)
aws lambda create-function-url-config \
  --function-name my-api-handler \
  --auth-type NONE \
  --cors '{
    "AllowOrigins": ["https://myapp.com"],
    "AllowMethods": ["GET","POST"],
    "AllowHeaders": ["Content-Type"]
  }'

# Output: https://<url-id>.lambda-url.eu-west-1.on.aws/

SQS Trigger

Lambda polls SQS and processes messages in batches. On failure, the batch is retried. Configure a DLQ on the SQS queue for messages that consistently fail.

// SQS event structure
export const handler = async (event) => {
  const results = [];

  for (const record of event.Records) {
    const message = JSON.parse(record.body);
    try {
      await processMessage(message);
      results.push({ itemIdentifier: record.messageId });
    } catch (err) {
      console.error('Failed to process:', record.messageId, err);
      // Don't throw — let successful messages be deleted
      // Only failed ones go back to queue (report batch item failures)
    }
  }
};

// Better: report partial batch failures
export const handler = async (event) => {
  const failures = [];
  for (const record of event.Records) {
    try {
      await processMessage(JSON.parse(record.body));
    } catch (err) {
      failures.push({ itemIdentifier: record.messageId });
    }
  }
  return { batchItemFailures: failures };
};
# Create SQS trigger
aws lambda create-event-source-mapping \
  --function-name my-worker \
  --event-source-arn arn:aws:sqs:eu-west-1:123:my-queue \
  --batch-size 10 \
  --maximum-batching-window-in-seconds 5 \
  --function-response-types ReportBatchItemFailures

S3 Event Trigger

// S3 event structure
export const handler = async (event) => {
  for (const record of event.Records) {
    const bucket = record.s3.bucket.name;
    const key = decodeURIComponent(record.s3.object.key.replace(/+/g, ' '));
    const size = record.s3.object.size;
    const eventType = record.eventName; // ObjectCreated:Put, ObjectRemoved:Delete, etc.

    console.log(`Event: ${eventType} | ${bucket}/${key} (${size} bytes)`);
    await processUploadedFile(bucket, key);
  }
};

EventBridge (Scheduled & Event-Driven)

# Schedule Lambda every 5 minutes (cron)
aws events put-rule \
  --name cleanup-job \
  --schedule-expression "rate(5 minutes)" \
  --state ENABLED

aws events put-targets \
  --rule cleanup-job \
  --targets Id=1,Arn=arn:aws:lambda:eu-west-1:123:function:cleanup

# Grant EventBridge permission to invoke Lambda
aws lambda add-permission \
  --function-name cleanup \
  --statement-id EventBridgeInvoke \
  --action lambda:InvokeFunction \
  --principal events.amazonaws.com \
  --source-arn arn:aws:events:eu-west-1:123:rule/cleanup-job

Lambda Destinations

Destinations route async invocation results to SNS, SQS, EventBridge, or another Lambda — for both success and failure. Preferred over DLQ for async functions.

aws lambda put-function-event-invoke-config \
  --function-name my-processor \
  --maximum-retry-attempts 2 \
  --destination-config '{
    "OnSuccess": {
      "Destination": "arn:aws:sqs:eu-west-1:123:success-queue"
    },
    "OnFailure": {
      "Destination": "arn:aws:sqs:eu-west-1:123:dead-letter-queue"
    }
  }'
Lambda

Performance, Concurrency & Cold Starts

Lambda: Performance, Concurrency & Cold Starts Cold Start Anatomy A cold start happens when Lambda initializes a new execution environment. The INIT phase: down

Lambda: Performance, Concurrency & Cold Starts

Cold Start Anatomy

A cold start happens when Lambda initializes a new execution environment. The INIT phase: download code/container → start runtime → run module-level code. Can add 100ms–3s depending on runtime and code size.

Phase          | What happens                            | When
---------------|----------------------------------------|---------------------------
Download       | Fetch deployment package from S3/ECR   | Cold start only
Runtime init   | Start Node.js/Python/JVM process       | Cold start only
Handler init   | Run module-level code (your init code) | Cold start only
Invoke         | Run handler function                   | Every invocation

Typical cold start durations (simple function, no VPC):
  Node.js:  50-200ms
  Python:   100-300ms
  Java:     1-5s (JVM startup) → use SnapStart
  Go:       <50ms (compiled binary)
  Container: 1-10s (image pull)

Reducing Cold Starts

  • Provisioned Concurrency: keep N environments pre-initialized, no cold starts for those

  • SnapStart (Java only): snapshot JVM state after init, restore snapshot on cold start — 90% faster

  • Right-size memory: more memory = faster network/CPU, faster INIT phase

  • Minimize package size: smaller ZIP = faster download; use tree-shaking, remove unused deps

  • Avoid VPC unless needed: VPC adds ~100ms for ENI attachment (improved with Hyperplane ENI)

  • Move init code outside handler: DB connections, SDK clients, parsed config

# Provisioned Concurrency (warm environments always ready)
aws lambda put-provisioned-concurrency-config \
  --function-name my-api-handler \
  --qualifier v5 \
  --provisioned-concurrent-executions 10

# Scale Provisioned Concurrency with Application Auto Scaling
aws application-autoscaling register-scalable-target \
  --service-namespace lambda \
  --resource-id function:my-api-handler:v5 \
  --scalable-dimension lambda:function:ProvisionedConcurrency \
  --min-capacity 5 \
  --max-capacity 50

Concurrency Model

Concurrency = (invocations/second) × (avg duration in seconds)

Types:
  Unreserved concurrency: shared pool for all functions in the region (default 1000)
  Reserved concurrency:   cap a function to N — never exceeds this, guaranteed minimum
  Provisioned concurrency: pre-warmed environments (subset of reserved)

Account default: 1000 concurrent executions per region
Request increase: via Service Quotas console (up to tens of thousands)

Throttling (429):
  Sync invocations: error returned immediately to caller
  Async invocations: retried automatically 2 times with backoff, then DLQ/destination
# Set reserved concurrency (also limits function to 100 max)
aws lambda put-function-concurrency \
  --function-name my-api-handler \
  --reserved-concurrent-executions 100

# Set to 0 to completely throttle function (emergency disable)
aws lambda put-function-concurrency \
  --function-name my-api-handler \
  --reserved-concurrent-executions 0

ARM64 (Graviton2)

Lambda supports ARM64 architecture (AWS Graviton2). Up to 34% better price/performance for many workloads. Simple runtime change — no code changes for Node.js/Python.

aws lambda update-function-configuration \
  --function-name my-api-handler \
  --architectures arm64

Lambda Power Tuning

  • Open source tool (AWS Step Functions): runs your function at 10 different memory sizes, finds optimal cost/performance

  • Use it before choosing memory settings for production

  • Often: doubling memory doubles cost per second but halves duration → same or cheaper total cost

  • Run at: github.com/alexcasalboni/aws-lambda-power-tuning

Ephemeral Storage (/tmp)

import { writeFileSync, readFileSync, existsSync } from 'fs';

// /tmp is shared across warm invocations in the same environment
// Cache expensive downloads across invocations
const CACHE_PATH = '/tmp/model-cache.bin';

export const handler = async (event) => {
  if (!existsSync(CACHE_PATH)) {
    // Cold start: download and cache
    const modelData = await downloadModel();
    writeFileSync(CACHE_PATH, modelData);
  }
  const model = readFileSync(CACHE_PATH);
  return predict(model, event.input);
};
Lambda

Deployment, Monitoring & Interview Questions

Lambda: Deployment, Monitoring & Interview Questions Deployment Methods # ZIP deployment (for code <250MB unzipped) zip -r function.zip . --exclude "*.test.js"

Lambda: Deployment, Monitoring & Interview Questions

Deployment Methods

# ZIP deployment (for code <250MB unzipped)
zip -r function.zip . --exclude "*.test.js" "node_modules/.cache/*"
aws lambda update-function-code \
  --function-name my-api-handler \
  --zip-file fileb://function.zip

# Container image deployment (up to 10GB)
# Build and push to ECR
aws ecr get-login-password --region eu-west-1 | \
  docker login --username AWS --password-stdin 123456789012.dkr.ecr.eu-west-1.amazonaws.com

docker build -t my-lambda .
docker tag my-lambda:latest 123456789012.dkr.ecr.eu-west-1.amazonaws.com/my-lambda:latest
docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/my-lambda:latest

# Update function to use new image
aws lambda update-function-code \
  --function-name my-api-handler \
  --image-uri 123456789012.dkr.ecr.eu-west-1.amazonaws.com/my-lambda:latest

Versions & Aliases

# Publish immutable version
aws lambda publish-version --function-name my-api-handler
# Returns version number (e.g., 7)

# Create alias pointing to version
aws lambda create-alias \
  --function-name my-api-handler \
  --name production \
  --function-version 7

# Canary deployment: 10% to new version, 90% to old
aws lambda update-alias \
  --function-name my-api-handler \
  --name production \
  --function-version 8 \
  --routing-config AdditionalVersionWeights={"7"=0.9}

# After validation: cut over to 100%
aws lambda update-alias \
  --function-name my-api-handler \
  --name production \
  --function-version 8

SAM (Serverless Application Model)

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

Globals:
  Function:
    Runtime: nodejs20.x
    MemorySize: 512
    Timeout: 30
    Environment:
      Variables:
        NODE_ENV: production

Resources:
  ApiHandler:
    Type: AWS::Serverless::Function
    Properties:
      Handler: dist/index.handler
      CodeUri: .
      Architectures: [arm64]
      Events:
        Api:
          Type: HttpApi
          Properties:
            Path: /{proxy+}
            Method: ANY
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref UsersTable

  UsersTable:
    Type: AWS::DynamoDB::Table
    Properties:
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: pk
          AttributeType: S
      KeySchema:
        - AttributeName: pk
          KeyType: HASH
# SAM CLI workflow
sam build
sam local invoke ApiHandler --event events/test.json   # Local testing
sam local start-api                                     # Local HTTP server
sam deploy --guided                                     # First deploy
sam deploy                                              # Subsequent deploys

CloudWatch Logs & Monitoring

# Lambda auto-creates log group: /aws/lambda/function-name
# Tail live logs
aws logs tail /aws/lambda/my-api-handler --follow

# Query with Insights
aws logs start-query \
  --log-group-name /aws/lambda/my-api-handler \
  --start-time $(date -d '1 hour ago' +%s) \
  --end-time $(date +%s) \
  --query-string 'fields @timestamp, @message
    | filter @message like /ERROR/
    | sort @timestamp desc
    | limit 50'
// Structured logging (JSON for CloudWatch Insights queries)
export const handler = async (event, context) => {
  const log = (level, msg, data = {}) => console.log(JSON.stringify({
    level, msg, requestId: context.awsRequestId, ...data
  }));

  log('info', 'Processing request', { path: event.rawPath });

  try {
    const result = await processRequest(event);
    log('info', 'Request completed', { durationMs: /* ... */ 0 });
    return result;
  } catch (err) {
    log('error', 'Request failed', { error: err.message, stack: err.stack });
    throw err;
  }
};

X-Ray Tracing

# Enable active tracing
aws lambda update-function-configuration \
  --function-name my-api-handler \
  --tracing-config Mode=Active
// Instrument AWS SDK calls automatically via X-Ray
import AWSXRay from 'aws-xray-sdk-core';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

// Wrap client to trace all calls
const ddbClient = AWSXRay.captureAWSv3Client(new DynamoDBClient({}));

Interview Questions

  • When NOT to use Lambda? Long-running tasks >15 min; stateful workloads; cold start latency is unacceptable; need full OS/GPU access; very high sustained throughput where EC2/containers are cheaper

  • How to handle Lambda cold starts? Provisioned Concurrency for latency-sensitive; SnapStart for Java; minimize package size; init clients outside handler; consider ARM64

  • Lambda vs Fargate? Lambda: event-driven, max 15min, simpler ops. Fargate: containerized, long-running, more control over runtime

  • How does Lambda scale? Automatically — one concurrent execution per request. Burst limit applies (initial scaling speed is capped per region). Reserved concurrency limits max scale.

  • How to make Lambda idempotent? Include idempotency key in requests; check if already processed (DynamoDB conditional writes); use Powertools Idempotency utility

  • Lambda and VPC? Putting Lambda in VPC adds cold start latency (ENI provisioning). Required for RDS access. Use RDS Proxy to reduce connection pressure. VPC Lambda needs NAT Gateway for internet access.

  • How is Lambda priced? Requests ($0.20/million) + Duration (GB-seconds, $0.0000166667 per GB-second). Provisioned Concurrency charged separately per GB-hour.

Keep your Lambda knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever