
Aws Serverless
- 4.8k installs
- 2.2k repo stars
- Updated August 4, 2026
- aws/agent-toolkit-for-aws
aws-serverless is an agent skill that guides building, deploying, debugging, and optimizing AWS serverless applications across Lambda, API Gateway, Step Functions, and EventBridge.
About
aws-serverless is an official AWS agent skill for serverless work across Lambda, API Gateway, Step Functions, EventBridge, DynamoDB streams, SQS, SNS, S3, and Kinesis integrations. It routes agents to reference files for architecture patterns, SAM and CDK deployment, troubleshooting, Lambda cold start and memory tuning, event source mappings, concurrency, API Gateway setup, orchestration, and production readiness checklists. The overview pairs with the AWS MCP server for CLI and CloudWatch validation while noting quota and runtime values may drift and should be confirmed before production. Routing sends new apps to architecture.md then deployment.md, errors to troubleshooting.md with five common fixes first, performance to lambda.md and production.md, and defers Lambda Managed Instances, durable functions, and microVM workloads to sibling skills. Developers reach for it when building event-driven AWS backends, debugging 502 or 504 API errors, tuning concurrency, configuring CORS, or optimizing cold starts and event mappings.
- Routes by user need to nine reference files covering Lambda through troubleshooting.
- Covers Lambda, API Gateway, Step Functions, EventBridge, and major event sources.
- Pairs with AWS MCP server for CLI, CloudWatch, and live configuration checks.
- Troubleshooting reference starts with the five most common serverless fixes.
- Defers LMI, durable functions, and microVM topics to dedicated sibling skills.
Aws Serverless by the numbers
- 4,806 all-time installs (skills.sh)
- +585 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #117 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
aws-serverless capabilities & compatibility
- Capabilities
- reference routing for architecture and deploymen · lambda performance and cold start optimization · event source mapping for sqs, ddb, sns, s3, kine · api gateway and orchestration configuration guid · production readiness checklist and anti patterns
- Works with
- aws
- Use cases
- api development · devops · testing
What aws-serverless says it does
Does not apply to EC2, ECS/Fargate containers, or Amplify hosting
npx skills add https://github.com/aws/agent-toolkit-for-aws --skill aws-serverlessAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4.8k |
|---|---|
| repo stars | ★ 2.2k |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | aws/agent-toolkit-for-aws ↗ |
How do I build or fix AWS serverless stacks covering Lambda, API Gateway, event sources, cold starts, and production readiness?
Build, deploy, debug, and optimize AWS serverless apps with Lambda, API Gateway, Step Functions, EventBridge, SAM, and CDK guidance.
Who is it for?
Developers and DevOps engineers working on AWS Lambda-centric event-driven architectures with SAM, CDK, or console configuration.
Skip if: Skip for EC2, ECS Fargate containers, Amplify hosting, or Lambda Managed Instances workloads covered by other skills.
When should I use this skill?
User mentions Lambda, API Gateway, Step Functions, SAM, CDK serverless, cold starts, CORS, 502 errors, or EventBridge patterns.
What you get
Architecture, deployment, tuning, or troubleshooting guidance routed to the correct AWS serverless reference with actionable fixes.
- Python Lambda handler module
- Powertools idempotency configuration
By the numbers
- Pre-wires 4 Powertools concerns: Logger, Tracer, Metrics, and Idempotency
Files
AWS Serverless
Overview
Domain expertise for building serverless applications on AWS. Covers Lambda configuration, API Gateway debugging, Step Functions orchestration, EventBridge patterns, event source mappings, concurrency tuning, cold start optimization, deployment with SAM/CDK, production readiness, and troubleshooting across all serverless services.
Works best with the AWS MCP server — enables running CLI commands, querying CloudWatch, and validating configurations directly. All guidance also works with standard AWS CLI access.
Note: Reference files contain specific runtime versions, quota values, and feature matrices that may change. When precision matters (e.g., deploying to production, choosing a runtime, or checking a quota), confirm values against current AWS documentation rather than relying solely on the values in these files.
Routing
| User need | Action |
|---|---|
| Building a new serverless app | Read architecture.md for pattern selection, then deployment.md for SAM/CDK templates |
| Debugging an error | Read troubleshooting.md — starts with the 5 most common fixes |
| Optimizing performance or cost | Read lambda.md for cold starts and memory tuning, production.md for readiness checklist |
| Configuring event sources (SQS, DDB Streams, SNS) | Read event-sources.md |
| Step Functions, EventBridge, or orchestration | Read orchestration.md |
| Concurrency configuration | Read concurrency.md |
| API Gateway setup | Read api-gateway.md |
| Common anti-patterns | Read the anti-patterns section in production.md |
| Starting with Powertools | Use powertools-handler.py as a template |
| Lambda Managed Instances, LMI, capacity providers, EC2-backed Lambda, PerExecutionEnvironmentMaxConcurrency | Use the aws-lambda-managed-instances skill instead |
| Durable functions, durable execution, checkpoint-and-replay | Use the aws-lambda-durable-functions skill instead |
| Firecracker microVMs, strong tenant isolation, sandboxed/untrusted code execution, long-lived sessions, suspend/resume, port-listening servers, snapshot-resumable compute | Use the aws-lambda-microvms skill instead |
| Spans multiple areas | Read the most specific reference first, then consult others as needed |
Files
| File | Content |
|---|---|
| lambda.md | Runtime, memory/CPU, cold starts, SnapStart, layers, containers |
| api-gateway.md | REST vs HTTP API, stages, auth, throttling, mapping |
| event-sources.md | SQS, DDB Streams, SNS, S3, Kinesis triggers |
| orchestration.md | Step Functions, EventBridge rules/pipes/scheduler |
| concurrency.md | Reserved vs provisioned, scaling, ESM concurrency |
| architecture.md | Patterns, reference architectures, service selection |
| deployment.md | SAM/CDK resource types, globals, fast iteration |
| production.md | Readiness checklist, observability, anti-patterns |
| troubleshooting.md | Error → cause → fix for all serverless services |
"""Lambda handler with Powertools Logger, Tracer, Metrics, and Idempotency wired."""
import json
from aws_lambda_powertools import Logger, Metrics, Tracer
from aws_lambda_powertools.metrics import MetricUnit
from aws_lambda_powertools.utilities.idempotency import (
DynamoDBPersistenceLayer,
IdempotencyConfig,
idempotent,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
logger = Logger()
tracer = Tracer()
metrics = Metrics()
persistence = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
# Idempotency key: "body" deduplicates identical payloads.
config = IdempotencyConfig(event_key_jmespath="body")
# Set log_event=True only in non-production environments;
# events may contain auth tokens, cookies, or PII.
@logger.inject_lambda_context(log_event=False)
@tracer.capture_lambda_handler
@metrics.log_metrics(capture_cold_start_metric=True)
@idempotent(config=config, persistence_store=persistence)
def handler(event: dict, context: LambdaContext) -> dict:
logger.info("Processing request")
result = process(event)
metrics.add_metric(name="RequestsProcessed", unit=MetricUnit.Count, value=1)
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "https://your-domain.example", # Replace with your domain
},
"body": json.dumps(result),
}
@tracer.capture_method
def process(event: dict) -> dict:
"""Replace with your business logic."""
return {"message": "success"}
API Gateway Reference
Quick-reference for REST API, HTTP API, WebSocket API — debugging, configuration, and quotas.
Contents
- REST vs HTTP API Comparison
- CORS Debugging
- Lambda Authorizers
- Throttling and Quotas
- WebSocket APIs
- 502/504 Debugging
---
REST vs HTTP API Comparison
Decision Tree
Need any of these? → REST API
├── API keys / usage plans / per-client throttling
├── Request validation (built-in)
├── Request/response body transformation (VTL)
├── Caching (built-in)
├── Private API endpoints
├── Edge-optimized endpoints
├── Canary deployments
├── Execution logs / X-Ray tracing
├── Resource policies
├── Mock integrations
└── Response streaming
None of the above? → HTTP API (lower latency, simpler)Feature Comparison
| Feature | REST API | HTTP API |
|---|---|---|
| Latency | Higher | Lower |
| Endpoint types | Edge, Regional, Private | Regional only |
| AWS WAF | Yes | No |
| API keys / usage plans | Yes | No |
| Per-client throttling | Yes | No |
| Request validation | Yes | No |
| Body transformation (VTL) | Yes | No |
| Parameter mapping | Yes | Yes |
| Caching (built-in) | Yes | No |
| Custom domains | Yes | Yes |
| Lambda authorizers | Yes (TOKEN + REQUEST) | Yes (REQUEST only) |
| JWT authorizers (native) | No | Yes |
| IAM auth | Yes | Yes |
| Cognito (native) | Yes | Yes (via JWT) |
| Resource policies | Yes | No |
| Mutual TLS | Yes | Yes |
| CORS setup | Manual OPTIONS method | Built-in config |
| Automatic deployments | No | Yes |
| Canary deployments | Yes | No |
| Custom gateway responses | Yes | No |
| Execution logs | Yes | No |
| Access logs (CloudWatch) | Yes | Yes |
| Access logs (Firehose) | Yes | No |
| X-Ray tracing | Yes | No |
| Mock integrations | Yes | No |
| Private integrations (NLB) | Yes | Yes |
| Private integrations (ALB) | Yes | Yes |
| Private integrations (Cloud Map) | No | Yes |
| Response streaming | Yes | No |
| Console test invocations | Yes | No |
| Integration timeout | 50ms–29s (configurable) | 30s hard max |
| Payload size | 10 MB | 10 MB |
REST API streaming caveats: Response streaming via REST API proxy integration does not support built-in caching, response transforms (VTL), or WAF inspection of streamed content. Idle timeouts apply, and a 2 MBps bandwidth cap applies after the first 10 MB (Function URLs apply the cap after 6 MB).
---
CORS Debugging
Proxy vs Non-Proxy
| Aspect | Proxy integration | Non-proxy integration |
|---|---|---|
| Who returns CORS headers? | Your Lambda function | API Gateway (method response) |
| OPTIONS method needed? | Yes (or use mock) | Yes (mock integration) |
| Where to configure? | In your code | In API Gateway console/IaC |
Debugging Flowchart
"Cross-Origin Request Blocked"?
│
├─ YES → Which integration type?
│ │
│ ├─ PROXY → Lambda MUST return CORS headers
│ │ ├─ Access-Control-Allow-Origin
│ │ ├─ Access-Control-Allow-Methods
│ │ └─ Access-Control-Allow-Headers
│ │
│ └─ NON-PROXY → Configure in API Gateway:
│ ├─ Create OPTIONS method (mock integration)
│ ├─ Add 200 response with CORS headers
│ └─ Add CORS headers to actual method responses
│
├─ OPTIONS returning 200?
│ ├─ NO → OPTIONS method missing or misconfigured
│ └─ YES → Check actual method response headers
│
└─ 502 on OPTIONS?
└─ Binary media types set to */* → fix belowCommon CORS Mistakes
| # | Mistake | Fix |
|---|---|---|
| 1 | No CORS headers in Lambda (proxy integration) | Add headers to every Lambda response |
| 2 | Missing OPTIONS method (REST API, non-proxy) | Create OPTIONS with mock integration |
| 3 | Binary media types */* breaks OPTIONS | Set contentHandling: CONVERT_TO_TEXT on OPTIONS |
| 4 | Allow-Origin: * with credentials: include | Specify exact origin, not wildcard |
| 5 | Not redeploying API after CORS changes | Redeploy the stage |
| 6 | Missing Allow-Headers for custom headers | List all headers the client sends |
| 7 | Gateway 4XX/5XX responses lack CORS headers | Add CORS headers to gateway responses |
Lambda CORS Headers — Python
def handler(event, context):
return {
"statusCode": 200,
"headers": {
"Access-Control-Allow-Origin": "https://example.com",
"Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT,DELETE",
"Access-Control-Allow-Headers": "Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token",
},
"body": json.dumps({"message": "success"}),
}Lambda CORS Headers — TypeScript
export const handler = async (event: any) => ({
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "https://example.com",
"Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT,DELETE",
"Access-Control-Allow-Headers": "Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token",
},
body: JSON.stringify({ message: "success" }),
});Binary Media Types */* Fix
# Fix OPTIONS integration request
aws apigateway update-integration \
--rest-api-id API_ID --resource-id RES_ID \
--http-method OPTIONS \
--patch-operations op='replace',path='/contentHandling',value='CONVERT_TO_TEXT'
# Fix OPTIONS integration response
aws apigateway update-integration-response \
--rest-api-id API_ID --resource-id RES_ID \
--http-method OPTIONS --status-code 200 \
--patch-operations op='replace',path='/contentHandling',value='CONVERT_TO_TEXT'---
Lambda Authorizers
TOKEN vs REQUEST Authorizer
| Feature | TOKEN | REQUEST |
|---|---|---|
| Identity source | Single header (bearer token) | Headers, query strings, stage vars, $context |
| Cache key | Token header value | All specified identity sources |
| Token validation regex | Yes | No |
| Fine-grained policies | Limited | Yes (multiple sources) |
| Available on | REST API only | REST API + HTTP API |
| Recommendation | Legacy | Preferred |
Use REQUEST authorizers for new APIs. TOKEN is legacy.
Caching Behavior
| Setting | Detail |
|---|---|
| Default TTL | 300 seconds |
| Range | 0 (disabled) – 3600 seconds |
| Cache key (TOKEN) | Header value from token source |
| Cache key (REQUEST) | All specified identity sources combined |
| Critical | Cached policy applies to ALL methods/resources |
If any specified identity source is missing/null/empty → 401 returned without invoking Lambda.
REQUEST Authorizer — Python
def lambda_handler(event, context):
token = event["headers"].get("Authorization", "")
is_authorized = verify_token(token) # Your auth logic
return {
"principalId": "user",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Action": "execute-api:Invoke",
"Effect": "Allow" if is_authorized else "Deny",
"Resource": event["methodArn"],
}],
},
"context": {"userId": "user", "scope": "read:items"},
}REQUEST Authorizer — TypeScript
import { APIGatewayAuthorizerResult, APIGatewayRequestAuthorizerEvent } from "aws-lambda";
export const handler = async (
event: APIGatewayRequestAuthorizerEvent
): Promise<APIGatewayAuthorizerResult> => {
const token = event.headers?.Authorization ?? "";
const isAuthorized = verifyToken(token); // Your auth logic
return {
principalId: "user",
policyDocument: {
Version: "2012-10-17",
Statement: [{
Action: "execute-api:Invoke",
Effect: isAuthorized ? "Allow" : "Deny",
Resource: event.methodArn,
}],
},
context: { userId: "user", scope: "read:items" },
};
};HTTP API JWT Authorizer (Native — No Lambda)
No Lambda function needed. Configure directly on the API:
# SAM / CloudFormation
MyHttpApi:
Type: AWS::Serverless::HttpApi
Properties:
Auth:
DefaultAuthorizer: MyJwtAuth
Authorizers:
MyJwtAuth:
AuthorizationScopes:
- read:items
IdentitySource: $request.header.Authorization
JwtConfiguration:
issuer: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_abc123
audience:
- my-client-idSupports any OIDC-compliant IdP (Cognito, Auth0, Okta, etc.).
---
Throttling and Quotas
Throttling Hierarchy (Applied in Order)
Most specific → Least specific:
1. Per-client / per-method (usage plan + API key) ← REST only
2. Per-method (stage method settings)
3. Account-level (all APIs in account/Region)
4. AWS Regional (hard limit, not changeable)Token Bucket Algorithm
- Tokens added at steady-state rate (RPS)
- Bucket holds up to burst capacity
- Each request = 1 token
- Empty bucket →
429 Too Many Requests - Burst allows temporary spikes above steady-state
Account-Level Defaults
| Quota | Default | Adjustable? |
|---|---|---|
| Steady-state RPS (per Region) | 10,000 | Yes |
| Burst capacity | 5,000 | Set by AWS based on RPS |
| Smaller Regions (Cape Town, Milan, Jakarta…) | 2,500 RPS / 1,250 burst | Yes |
REST API Quotas
| Resource | Default | Adjustable? |
|---|---|---|
| Integration timeout | 50ms–29s (default 29s) | Yes (Regional/private only) |
| Payload size | 10 MB | No |
| Header value size | 10,240 bytes | No |
| Cache TTL | 0–3600s | No |
| Resources per API | 300 | Yes |
| Stages per API | 10 | Yes |
| API keys per account | 10,000 | No |
| Usage plans per account | 300 | Yes |
| Custom domains per Region | 120 | Yes |
| Mapping template size | 300 KB | No |
HTTP API Quotas
| Resource | Default | Adjustable? |
|---|---|---|
| Integration timeout | 30s max | No |
| Payload size | 10 MB | No |
| Routes per API | 300 | Yes |
| Stages per API | 10 | Yes |
| Integrations per API | 300 | No |
| Custom domains per Region | 120 | Yes |
| VPC links per Region | 10 | Yes |
Usage Plans (REST API Only)
- Per-client rate limits (RPS) and burst limits via API keys
- Daily/weekly/monthly quotas per key
- Method-level throttling within a plan (e.g.,
GET /pets= 100 RPS)
Client-Side 429 Handling
- Exponential backoff with jitter
- Respect
Retry-Afterheader - Client-side rate limiting to stay under known limits
---
WebSocket APIs
Route Architecture
Client connects → $connect (auth, store connectionId)
Client sends msg → route selection → custom route or $default
Server pushes data → @connections API (POST to connectionId)
Client disconnects → $disconnect (cleanup connectionId)Route Selection
- Expression:
$request.body.action(routes on JSONactionfield) - Non-JSON messages → always
$default
Predefined Routes
| Route | When | Required? | Notes |
|---|---|---|---|
$connect | Connection initiated | No | Auth here; connection pending until integration completes |
$disconnect | Connection closed | No | Best-effort; connection already closed |
$default | No matching route / non-JSON | No | Catch-all fallback |
Connection Management — Python
import boto3, json
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("WebSocketConnections")
def connect_handler(event, context):
table.put_item(Item={"connectionId": event["requestContext"]["connectionId"]})
return {"statusCode": 200, "body": "Connected"}
def send_to_client(endpoint_url, connection_id, data):
client = boto3.client("apigatewaymanagementapi", endpoint_url=endpoint_url)
client.post_to_connection(
ConnectionId=connection_id,
Data=json.dumps(data).encode("utf-8"),
)Connection Management — TypeScript
import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi";
async function sendToClient(endpoint: string, connectionId: string, data: object) {
const client = new ApiGatewayManagementApiClient({ endpoint });
await client.send(new PostToConnectionCommand({
ConnectionId: connectionId,
Data: Buffer.from(JSON.stringify(data)),
}));
}WebSocket Quotas
| Resource | Limit |
|---|---|
| Idle connection timeout | 10 minutes |
| Max connection duration | 2 hours |
| Message payload | 128 KB (hard limit) |
WebSocket Close Codes
| Code | Meaning |
|---|---|
| 1001 | Idle timeout or max duration exceeded |
| 1003 | Unsupported binary media type |
| 1005 | No status code present (reserved, not sent on wire) |
| 1006 | Abnormal closure — no close frame received |
| 1008 | Throttled (too many requests) |
| 1009 | Message exceeds size limit |
| 1011 | Internal server error |
| 1012 | Service restart |
---
502/504 Debugging
502 Bad Gateway — Flowchart
502 Bad Gateway
│
├─ Lambda proxy integration?
│ └─ YES → Check response format (most common cause):
│ ├─ statusCode: integer (string is coerced, missing defaults to 200)
│ ├─ headers: object with string values
│ ├─ body: string (JSON.stringify, not raw object)
│ └─ Unhandled exception? → Check CloudWatch Logs
│
├─ Lambda authorizer?
│ ├─ Must return valid IAM policy format
│ ├─ Check authorizer Lambda logs
│ └─ Authorizer timeout is separate from integration timeout
│
├─ HTTP integration?
│ ├─ Backend reachable from API Gateway?
│ ├─ Valid HTTP response from backend?
│ └─ VPC link healthy? (private integration)
│
└─ Other causes:
├─ Payload > 10 MB
├─ Binary media types */* (breaks OPTIONS)
└─ Stage variable → wrong Lambda aliasCorrect Lambda Response Format
The most common cause of 502 is an incorrect response format in Lambda proxy integrations.
Python — Correct:
def handler(event, context):
return {
"isBase64Encoded": False, # boolean
"statusCode": 200, # integer, NOT string
"headers": { # object with string values
"Content-Type": "application/json",
},
"body": json.dumps({"key": "val"}) # MUST be string
}TypeScript — Correct:
export const handler = async (event: any) => ({
isBase64Encoded: false,
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "val" }), // MUST be string
});Common mistakes -> 502:
return {"statusCode": 200, "body": {"key": "val"}} # body not a string -> 502
return "just a string" # not a JSON object -> 502
# Note: string statusCode ("200") and missing statusCode are silently handled (no 502)504 Timeout — Flowchart
504 Endpoint Request Timed Out
│
├─ Step 1: Enable CloudWatch logging
│ ├─ REST: execution logs + access logs
│ ├─ HTTP: access logs only
│ └─ Include: $context.integrationLatency, $context.integration.status
│
├─ Step 2: Identify timeout source
│ ├─ REST API: integration timeout configurable 50ms–29s
│ ├─ HTTP API: 30s max (can be lowered, cannot be raised)
│ └─ Was integration invoked?
│ ├─ NO → Transient network failure; retry
│ └─ YES → Backend too slow
│
├─ Step 3: Reduce integration runtime
│ ├─ Move non-critical work to async (SQS, Step Functions)
│ ├─ Increase Lambda memory (faster CPU)
│ ├─ Provisioned concurrency (eliminate cold starts)
│ └─ Check downstream dependencies (DB, external APIs)
│
└─ Step 4: Increase timeout (REST only)
├─ Request via Service Quotas console
├─ Update integration timeout value AND redeploy
└─ Note: may reduce account throttle quotaCloudWatch Insights Queries
Find all 5xx errors:
fields @timestamp, @message, @logStream
| filter status >= 500 and status < 600
| sort @timestamp desc
| display @timestamp, httpMethod, resourcePath, status, requestIdFind timeout errors:
fields @timestamp, @message
| filter @message like "Execution failed due to a timeout error"
| sort @timestamp descFind slow integrations (>10s):
fields @timestamp, integrationLatency, status, resourcePath
| filter integrationLatency > 10000
| sort integrationLatency descAutomated Troubleshooting
AWSSupport-TroubleshootAPIGatewayHttpErrors — Systems Manager runbook:
- Validates API, resource, operation, and stage
- Analyzes CloudWatch logs automatically
- Requires:
apigateway:GET,logs:GetQueryResults,logs:StartQuery,ssm:* - Available in Systems Manager console → Automation
Serverless Architecture Patterns
Reference architectures, pattern selection flowcharts, and service selection tables for common serverless workloads.
Contents
- Pattern selection flowchart
- REST/HTTP API pattern
- Event processing pattern
- Orchestration pattern
- Real-time streaming pattern
- Async fan-out pattern
- Scheduled jobs pattern
- Choosing between patterns
---
Pattern selection flowchart
What are you building?
│
├── Synchronous request/response API?
│ └── REST/HTTP API pattern
│
├── Processing events from a queue/stream/database?
│ └── Event processing pattern
│
├── Multi-step workflow with branching/error handling?
│ └── Orchestration pattern
│
├── Real-time bidirectional communication or LLM streaming?
│ └── Real-time streaming pattern
│
├── One event triggers multiple independent consumers?
│ └── Async fan-out pattern
│
└── Recurring task on a schedule?
└── Scheduled jobs pattern---
REST/HTTP API pattern
Client → API Gateway (HTTP API) → Lambda → DynamoDB
→ S3 (binary storage)When: CRUD APIs, mobile/web backends, microservices.
Service selection:
| Decision | Default | Alternative |
|---|---|---|
| API type | HTTP API (simpler) | REST API if you need WAF, caching, request validation, API keys |
| Auth | JWT authorizer (HTTP API native) | Cognito (REST: native Cognito authorizer; HTTP: JWT authorizer), Lambda authorizer (custom logic) |
| Database | DynamoDB (on-demand) | RDS Proxy + RDS if relational data needed |
| File storage | S3 with presigned URLs | Direct upload via API Gateway (10 MB limit) |
| Function pattern | One function per route | Lambdalith if team prefers Express/FastAPI style |
Key constraints:
- HTTP API: 30s hard timeout, no WAF, no caching, 10 MB payload
- REST API: 29s default timeout (adjustable for Regional/private APIs), 10 MB payload
---
Event processing pattern
Event source → SQS → Lambda → DynamoDB / S3
↓
DLQ (failed messages)When: Async workloads, decoupled producers/consumers, batch processing, file processing.
Service selection:
| Decision | Default | Alternative |
|---|---|---|
| Buffer | SQS standard queue | SQS FIFO if ordering matters (10 msg batch limit) |
| Trigger | SQS event source mapping | S3 event notification → Lambda (file uploads) |
| Change data capture | DynamoDB Streams → Lambda | EventBridge Pipes → Lambda (no ESM needed) |
| Stream ingestion | SQS (simpler) | Kinesis (ordered replay, multiple consumers, high-throughput) |
| Error handling | SQS redrive policy (DLQ) | On-failure destination (SQS/SNS/S3) for streams |
| Concurrency control | MaximumConcurrency on ESM | Reserved concurrency on function |
| Batch processing | ReportBatchItemFailures | Powertools Batch Processor utility |
Key constraints:
- SQS visibility timeout ≥ 6× function timeout
- MaximumConcurrency and Provisioned Mode are mutually exclusive on same ESM
- Enable partial batch failure reporting to avoid reprocessing successful messages
- SQS event filtering automatically deletes unmatched messages (permanently — not sent to DLQ)
S3 trigger constraints:
- Recursive invocation risk: never write output to the same bucket/prefix that triggers the function
- No native DLQ on S3 notifications — use Lambda async invocation DLQ instead
- Use prefix/suffix filtering to limit which objects trigger the function
- Consider EventBridge for S3 instead of S3 notifications (richer filtering, multiple targets)
DynamoDB Streams constraints:
- Max 2 Lambda consumers per stream shard (use EventBridge Pipes for more)
- 24-hour stream retention — records expire and cannot be replayed after that
- Ordering guaranteed per partition key, not globally
---
Orchestration pattern
Trigger → Step Functions → Lambda (validate)
→ Choice (route by status)
→ Parallel (fan-out)
→ Lambda (aggregate) → DynamoDBWhen: Multi-step workflows, saga transactions, approval chains, data pipelines, AI agent loops.
Service selection:
| Decision | Default | Alternative |
|---|---|---|
| Workflow type | Standard (exactly-once, up to 1 year) | Express (<5 min, high-volume; async=at-least-once, sync=at-most-once) |
| Simple data transforms | JSONata (inline, no Lambda needed) | Lambda task (complex logic) |
| Service calls | Direct SDK integration (200+ services) | Lambda intermediary (only if business logic needed) |
| Human approval | .waitForTaskToken | Lambda durable functions waitForCallback |
| AI agent loops | Step Functions + Bedrock | Lambda durable functions (code-first, checkpointed) |
| Error handling | Retry + Catch in ASL | Lambda durable functions try/catch in code |
Key constraints:
- 256 KB payload limit between states — use S3 for large data
- Express: no .sync, no .waitForTaskToken, no Distributed Map, no Activities
- 25,000 execution history entries (Standard) — split long workflows into child executions
- Prefer direct SDK integrations over Lambda intermediary functions to reduce latency
---
Real-time streaming pattern
Client ←→ API Gateway WebSocket ←→ Lambda → DynamoDB (connections)
→ Bedrock (LLM responses)Or for LLM token streaming:
Client → Lambda Function URL (streaming) → Bedrock ConverseStreamWhen: Chat apps, live dashboards, notifications, LLM token streaming, multiplayer games.
Service selection:
| Decision | Default | Alternative |
|---|---|---|
| Bidirectional | API Gateway WebSocket | AppSync subscriptions (GraphQL) |
| LLM streaming | Lambda Function URL + ConverseStream | REST API proxy with STREAM mode |
| Connection state | DynamoDB (connectionId → metadata, enable TTL to clean up stale connections after 2-hour max duration) | ElastiCache (higher throughput) |
| Auth | $connect route authorizer | Cognito + custom auth in Lambda |
Key constraints:
- WebSocket: 10 min idle timeout, 2 hour max connection, 128 KB message (hard limit)
- Function URL streaming: 200 MB limit, 2 MBps after first 6 MB, Node.js native support
- Function URLs MUST use
AWS_IAMauth type. For CloudFront integration, use Origin Access Control (OAC) to sign requests — do not set auth toNONE. IfNONEis unavoidable for other reasons, authentication MUST be enforced at the edge (e.g., CloudFront + Lambda@Edge). No native JWT/Cognito support.
---
Async fan-out pattern
Producer → EventBridge → Rule A → Lambda (process)
→ Rule B → Step Functions (workflow)
→ Rule C → SQS → Lambda (batch)When: One event triggers multiple independent actions, event-driven microservices, cross-service communication.
Service selection:
| Decision | Default | Alternative |
|---|---|---|
| Event router | EventBridge (content-based routing) | SNS (simpler fan-out, attribute/body filtering) |
| Point-to-point | EventBridge Pipes (source→target, no Lambda intermediary) | SQS → Lambda ESM |
| Schema management | EventBridge Schema Registry + Discovery | Manual schema documentation |
| Cross-account | EventBridge cross-account rules | SNS cross-account subscriptions |
| Scheduling | EventBridge Scheduler (cron/rate) | EventBridge rules (simpler but less flexible) |
Key constraints:
- Use dedicated event bus per application domain (not the default bus)
- EventBridge Pipes eliminates Lambda intermediary functions for source→target integrations
- Be precise with event patterns — overly broad patterns risk loops
- Configure DLQs on all targets
---
Scheduled jobs pattern
EventBridge Scheduler → Lambda (task)
→ Step Functions (complex workflow)When: Cron jobs, periodic data sync, report generation, cleanup tasks.
Service selection:
| Decision | Default | Alternative |
|---|---|---|
| Scheduler | EventBridge Scheduler (flexible, one-time + recurring) | EventBridge rules with schedule expression (simpler) |
| Short task (<15 min) | Lambda directly | — |
| Long task (>15 min) | Step Functions (up to 1 year) | Lambda durable functions |
| High frequency (<1 min) | Not supported natively | SQS delay queue + Lambda |
Key constraints:
- Minimum schedule interval: 1 minute
- Lambda max timeout: 15 minutes — use Step Functions for longer
- Always make scheduled Lambda idempotent (scheduler guarantees at-least-once)
- Use EventBridge Scheduler over EventBridge rules for new projects (more features, flexible time windows)
---
Choosing between patterns
Most real applications combine multiple patterns:
┌─ HTTP API ─── Lambda ─── DynamoDB
Client ─── CloudFront ─┤
└─ WebSocket ── Lambda ─── DynamoDB
│
▼
EventBridge
┌────┼────┐
▼ ▼ ▼
SQS SFN Lambda
│ │
▼ ▼
Lambda BedrockCommon combinations:
| Application | Patterns used |
|---|---|
| SaaS API backend | REST API + Event processing + Scheduled jobs |
| E-commerce | REST API + Orchestration (order saga) + Fan-out (notifications) |
| Data pipeline | Scheduled jobs + Event processing + Orchestration |
| AI chatbot | Real-time streaming + Orchestration (agent loop) |
| IoT processing | Event processing + Fan-out + Scheduled jobs (aggregation) |
Begin with a single pattern and add more as requirements grow. A CRUD API with DynamoDB covers most initial implementations. Add event processing when you need async work. Add orchestration when you need multi-step workflows. Add fan-out when you need cross-service communication.
Lambda Concurrency Controls
Four concurrency controls operate at different levels, solve different problems, and have complex interactions.
Contents
- The 4 concurrency types
- Interaction matrix
- Decision scenarios
- Account limits and scaling
- Common mistakes
- SnapStart interaction
- SAM/CDK examples
---
The 4 concurrency types
1. Reserved Concurrency
Sets the maximum concurrent instances for a function and reserves that capacity from the account pool so no other function can consume it.
- Scope: Function.
- Reserve 400 → function always gets up to 400, never more. Others share the rest.
- Setting to 0 completely throttles the function (emergency shutoff).
- Use for: protecting critical functions, capping to protect downstream, emergency shutoff.
2. Provisioned Concurrency
Pre-initializes execution environments so they are ready before requests arrive.
- Scope: Published version or alias (NOT
$LATEST). - Allocation rate: Up to 6,000 environments per minute when provisioning.
- Configure 100 on alias
PROD→ first 100 concurrent requests get sub-10ms startup.
Request 101+ spills to on-demand with cold starts.
- Account-level RPS quota: RPS = 10 × account concurrency. For example, 1,000 account concurrency → 10,000 RPS cap across all functions. This is an account-level quota, not a per-instance throughput cap. Per-instance throughput = 1 / function duration.
- Combine with Application Auto Scaling (target ~70% utilization).
- Use for: user-facing APIs, functions with heavy init (ML models, DB pools).
3. Maximum Concurrency
Limits how many concurrent instances a specific SQS event source mapping (ESM) can invoke.
- Scope: Per ESM. Range: 2–1,000. Sources: SQS only.
- Does not reserve anything — other triggers can still consume function concurrency.
- Use for: multiple SQS queues on one function, rate-limiting a specific queue.
4. Provisioned Mode — ESM (Kafka 2024, SQS 2025)
Allocates dedicated event pollers for an SQS or Kafka ESM with configurable min/max.
- Scope: Per ESM.
- Standard mode: ~5 pollers, +300/min, max 1,250 invokes. Provisioned mode: you control
min/max pollers. Each handles up to 1 MB/s, 10 concurrent invokes.
- Use for: high-throughput SQS/Kafka, spiky traffic where standard ramp-up is too slow.
---
Interaction matrix
| Combination | OK? | Notes |
|---|---|---|
| Reserved + Provisioned | Yes | Provisioned ≤ Reserved |
| Reserved + Max Concurrency (ESM) | Yes | Reserved ≥ Σ(max concurrency across ESMs) |
| Reserved + Provisioned Mode (ESM) | Yes | Independent layers |
| Provisioned + Max Concurrency (ESM) | Yes | Different layers |
| Provisioned + Provisioned Mode (ESM) | Yes | Warms envs vs warms pollers |
| Max Concurrency + Provisioned Mode (same ESM) | No | Mutually exclusive |
| Provisioned Concurrency + SnapStart | No | Mutually exclusive |
Key rules: Account limit is the hard ceiling. Reserved carves from the pool — Lambda always keeps 100 unreserved. Provisioned ≤ Reserved when both set. Max Concurrency is advisory to the ESM, not the function.
┌──────────────────────────────────────────────────────┐
│ ACCOUNT: 1,000 concurrency │
│ ┌─────────────────┐ ┌───────────────────────────┐ │
│ │ RESERVED (400) │ │ UNRESERVED POOL (600) │ │
│ │ ┌─────────────┐ │ │ Shared by all others │ │
│ │ │PROVISIONED │ │ │ Must keep ≥100 always │ │
│ │ │(200 warm) │ │ └───────────────────────────┘ │
│ │ └─────────────┘ │ │
│ │ + 200 on-demand │ ESM LAYER (per mapping): │
│ └─────────────────┘ Max Concurrency — OR — │
│ Provisioned Mode (not both) │
└──────────────────────────────────────────────────────┘---
Decision scenarios
| Scenario | Reserved | Provisioned | Max Conc (ESM) | Prov Mode (ESM) |
|---|---|---|---|---|
| Protect critical API from starvation | Yes | — | — | — |
| Cap function to protect downstream DB | Yes | — | — | — |
| Eliminate cold starts for user-facing API | Optional | Yes | — | — |
| Multiple SQS queues, prevent hogging | Yes | — | Yes | — |
| High-throughput SQS, low-latency | Optional | Optional | — | Yes |
| Kafka ESM with spiky traffic | — | — | — | Yes |
| Predictable daily traffic | — | Yes+AutoScale | — | — |
| Emergency shutoff | Yes (=0) | — | — | — |
| Java/.NET heavy init | — | Yes or SnapStart | — | — |
A — Checkout API: Reserved=200 + Provisioned=150 + Auto Scaling for peak. B — 3 SQS queues → 1 function: Reserved=300, Max Concurrency=100 per ESM. C — Kafka stream (spiky): Provisioned Mode min=5, max=50 pollers. D — Batch job: Reserved=50, no provisioned.
---
Account limits and scaling
| Quota | Default | Adjustable? |
|---|---|---|
| Account concurrency | 1,000 / Region | Yes |
| Reservable concurrency | Account − 100 | Scales |
| RPS limit | 10 × concurrency | Scales |
| Scaling rate | 1,000 envs / 10s / function | No |
Scaling is per-function, continuously refilled, unused capacity does not accumulate. ~50 seconds to reach 5,000 concurrency from zero.
At the limit: Sync → 429. Async → retries up to 6h then DLQ. Streams → polling throttled, messages stay in source.
RPS constraint: A 50ms function at 20,000 RPS needs only 1,000 concurrency but the RPS limit (10×1,000=10,000) throttles it. Request account concurrency = 2,000.
aws service-quotas request-service-quota-increase \
--service-code lambda --quota-code L-B99A9384 --desired-value 5000---
Common mistakes
1. Reserved set to 0 — Blocks ALL invocations (429 TooManyRequestsException). Sometimes set during an incident and not restored. If a function is throttled at low traffic, check this first.
2. Reserved too low — Reserve 50, need 80 → throttled at 51 even with spare account capacity. Fix: monitor ConcurrentExecutions, set above peak + buffer.
3. Starving other functions — Reserve 800/1,000 → others share 200. Reserved is subtracted even when unused. Fix: be conservative.
4. Provisioned without auto scaling — Paying for idle envs off-peak, spilling on-peak. Fix: Auto Scaling targeting ~70% ProvisionedConcurrencyUtilization.
5. Provisioned on `$LATEST` — Doesn't work. Fix: publish a version, create an alias.
6. Max concurrency > reserved — ESM tries 100, function caps at 50. Fix: ensure reserved ≥ Σ(max concurrency across ESMs).
7. Confusing ESM max with reserved — Max concurrency doesn't reserve anything. API Gateway can still consume all concurrency. Fix: use reserved on the function.
8. Both ESM controls on same ESM — Mutually exclusive; API rejects it. Fix: choose one.
9. Forgetting 100-unit buffer — Max reservable = account limit − 100.
10. Not tracking ClaimedAccountConcurrency — Provisioned counts against account limit even when idle. Monitor the metric.
---
SnapStart interaction
| Aspect | SnapStart | Provisioned Concurrency |
|---|---|---|
| Cold start | Seconds → sub-second | Seconds → ~0 |
| Runtimes | Java 11+, Python 3.12+, .NET 8+ | All |
| Scales with traffic | Yes (snapshot restore) | Only up to provisioned count |
SnapStart and Provisioned Concurrency are mutually exclusive on the same function.
Is runtime Java 11+, Python 3.12+, or .NET 8+?
├─ No → Provisioned Concurrency
└─ Yes
├─ Need guaranteed <50ms on EVERY request? → Provisioned Concurrency
├─ Need EFS or >512MB ephemeral storage? → Provisioned Concurrency
└─ Otherwise → SnapStart first; if P99 still too high, switch to Provisioned Concurrency (they cannot coexist)Limitations: no EFS, no >512MB ephemeral, no container images, must handle uniqueness, re-validate network connections on restore.
---
SAM/CDK property reference
| Concurrency type | SAM property | CDK property |
|---|---|---|
| Reserved | ReservedConcurrentExecutions: 100 | reservedConcurrentExecutions: 100 |
| Provisioned | AutoPublishAlias: live + ProvisionedConcurrencyConfig.ProvisionedConcurrentExecutions: 50 | new lambda.Alias({ provisionedConcurrentExecutions: 50 }) — must use alias, not $LATEST |
| Maximum Concurrency (ESM) | ScalingConfig.MaximumConcurrency: 50 | maxConcurrency: 50 on EventSourceMapping |
| Provisioned Mode (ESM) | ProvisionedPollerConfig.MinimumPollers / MaximumPollers | provisionedPollerConfig: { minimumPollers, maximumPollers } on EventSourceMapping |
| SnapStart | SnapStart.ApplyOn: PublishedVersions + AutoPublishAlias | snapStart: lambda.SnapStartConf.ON_PUBLISHED_VERSIONS |
Auto scaling for Provisioned Concurrency: alias.addAutoScaling({ minCapacity, maxCapacity }) then scaling.scaleOnUtilization({ utilizationTarget: 0.7 }).
Deployment Reference
Serverless-specific deployment patterns, resource types, and fast iteration tools.
Contents
---
SAM resource types
SAM templates extend CloudFormation with Transform: AWS::Serverless-2016-10-31. Only Transform and Resources are required.
| Resource Type | Purpose |
|---|---|
AWS::Serverless::Function | Lambda + IAM role + event source mappings |
AWS::Serverless::HttpApi | HTTP API (API Gateway v2) — recommended |
AWS::Serverless::Api | REST API (v1) — WAF, usage plans, request validation |
AWS::Serverless::SimpleTable | DynamoDB with minimal config |
AWS::Serverless::LayerVersion | Lambda layer |
AWS::Serverless::StateMachine | Step Functions state machine |
AWS::Serverless::Connector | Simplified permissions between resources |
AWS::Serverless::Application | Nested serverless application (SAR or local) |
AWS::Serverless::GraphQLApi | AppSync GraphQL API |
AWS::Serverless::WebSocketApi | WebSocket API (API Gateway v2) |
AWS::Serverless::CapacityProvider | Lambda Managed Instances on customer-owned EC2 |
---
SAM Globals section
Eliminates duplication across functions/APIs. Supported types: Function, Api, HttpApi, SimpleTable, StateMachine, CapacityProvider.
Override rules:
| Type | Behavior |
|---|---|
| Primitives (string, number, boolean) | Resource value replaces global |
| Maps (dictionaries) | Merged — resource keys override matching global keys |
| Lists (arrays) | Global entries prepended to resource entries |
---
CDK serverless constructs
Prefer L2 constructs — they provide sensible defaults and least-privilege IAM via grant* methods.
| Construct | Module | Use for |
|---|---|---|
NodejsFunction | aws-cdk-lib/aws-lambda-nodejs | Node.js/TypeScript — bundles with esbuild automatically |
PythonFunction | @aws-cdk/aws-lambda-python-alpha | Python — requires Docker for bundling |
HttpApi | aws-cdk-lib/aws-apigatewayv2 | HTTP API with CORS, JWT auth |
HttpLambdaIntegration | aws-cdk-lib/aws-apigatewayv2-integrations | Connect Lambda to HttpApi |
---
Fast iteration
Both tools are development-only — they bypass CloudFormation safety and introduce drift. Use sam deploy or CI/CD for production.
SAM Accelerate
sam sync --watch --stack-name my-stack # Watch mode — auto-syncs on save
sam sync --code --watch --stack-name my-stack # Code-only (minimal sync time)
sam sync --code --resource-id MyFunction --watch --stack-name my-stack # Single functionCode changes sync via service APIs in seconds. Infrastructure changes trigger CloudFormation (slower, automatic).
CDK hotswap / watch
cdk deploy --hotswap # Direct resource update, skips non-hotswappable
cdk deploy --hotswap-fallback # Hotswap with CloudFormation fallback
cdk watch # Watch mode (hotswap + file watching)Hotswap supports: Lambda code/config/versions/aliases, Step Functions definitions, ECS images, S3 deployments, CodeBuild projects, AppSync resolvers/functions/schemas.
Comparison
| Feature | SAM Sync | CDK Hotswap |
|---|---|---|
| Watch mode | sam sync --watch | cdk watch |
| Code-only sync | sam sync --code | cdk deploy --hotswap |
| Fallback to full deploy | Automatic | --hotswap-fallback |
| Selective resource sync | --resource-id | Not supported |
| Code change speed | Seconds | Seconds |
| Production safe | No | No |
Lambda Event Sources Reference
Quick reference for Lambda event source mappings (ESMs), direct triggers, filtering, and error handling.
Contents
- SQS event source mapping
- DynamoDB Streams triggers
- SNS subscriptions
- Event filtering
- Partial batch failure reporting
- Error handling strategies
---
SQS event source mapping
Lambda polls SQS using long polling and invokes your function synchronously with a batch of messages.
Configuration parameters
| Parameter | Default | Range / Notes |
|---|---|---|
BatchSize | 10 | Standard: max 10,000. FIFO: max 10 |
MaximumBatchingWindowInSeconds | 0 | 0–300. Not supported for FIFO. Requires ≥ 1s when BatchSize > 10 |
MaximumConcurrency | — | 2–1,000. Per-ESM concurrency cap |
ProvisionedPollerConfig.MinimumPollers | 2 | 2–200 |
ProvisionedPollerConfig.MaximumPollers | 200 | 2–2,000 |
FilterCriteria | — | Filters on body key only |
FunctionResponseTypes | — | Set to ReportBatchItemFailures |
MaximumConcurrency and Provisioned Mode are mutually exclusive. You cannot set both on the same ESM.
Batching behavior
Lambda invokes when any condition is met:
1. Batching window expires 2. Batch size reached 3. Payload reaches 6 MB
Scaling behavior
Standard queues:
- Starts with 5 concurrent invocations
- Scales up by 300/min
- Default maximum: 1,250 concurrent invocations
- Provisioned mode: up to 20,000 (scales 3× faster at 1,000/min)
FIFO queues:
- Concurrency capped by the lower of: number of message group IDs or
MaximumConcurrency - Messages delivered in order per message group ID
Error handling
- Use the SQS redrive policy (native dead-letter queue (DLQ) on the queue) — not an ESM-level DLQ
- Set visibility timeout to ≥ 6× function timeout to prevent premature retry
- On function error, entire batch becomes visible again after visibility timeout
- On throttle, Lambda backs off; messages reappear after visibility timeout
SAM template
MyFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs22.x
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt MyQueue.Arn
BatchSize: 10
MaximumBatchingWindowInSeconds: 5
FunctionResponseTypes:
- ReportBatchItemFailures
ScalingConfig:
MaximumConcurrency: 50
FilterCriteria:
Filters:
- Pattern: '{"body": {"status": ["PENDING"]}}'CDK example
import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
import * as sqs from 'aws-cdk-lib/aws-sqs';
const dlq = new sqs.Queue(this, 'DLQ');
const queue = new sqs.Queue(this, 'MyQueue', {
visibilityTimeout: Duration.seconds(300), // 6× function timeout
deadLetterQueue: { queue: dlq, maxReceiveCount: 3 },
});
fn.addEventSource(new SqsEventSource(queue, {
batchSize: 10,
maxBatchingWindow: Duration.seconds(5),
reportBatchItemFailures: true,
maxConcurrency: 50,
}));---
DynamoDB Streams triggers
Lambda polls DynamoDB stream shards at 4 times per second. Invokes synchronously with in-order processing at the partition-key level.
Configuration parameters
| Parameter | Default | Range / Notes |
|---|---|---|
BatchSize | 100 | Max 10,000 |
MaximumBatchingWindowInSeconds | 0 | 0–300 |
StartingPosition | — | TRIM_HORIZON (recommended) or LATEST |
ParallelizationFactor | 1 | 1–10. Concurrent batches per shard |
BisectBatchOnFunctionError | false | Split failed batch in half |
MaximumRetryAttempts | -1 (infinite) | 0–10,000 |
MaximumRecordAgeInSeconds | -1 (infinite) | -1 to 604,800 (7 days) |
DestinationConfig.OnFailure | — | SQS, SNS, S3, or Kafka topic |
FilterCriteria | — | Filters on dynamodb key and metadata fields (e.g., eventName) |
FunctionResponseTypes | — | ReportBatchItemFailures |
TumblingWindowInSeconds | — | 0–900 for stateful aggregation |
Key behaviors
- TRIM_HORIZON recommended —
LATESTmay miss events during ESM creation - Max 2 Lambda readers per shard (single-region tables). Global tables: limit to 1
- ParallelizationFactor: 100 shards × factor 10 = up to 1,000 concurrent invocations. Order maintained at partition-key level
- BisectBatchOnFunctionError does NOT consume retry quota
- DynamoDB stream retention is 24 hours — a poison record can block a shard for that entire window without retry limits
SAM template
MyFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs22.x
Events:
DDBStream:
Type: DynamoDB
Properties:
Stream: !GetAtt MyTable.StreamArn
StartingPosition: TRIM_HORIZON
BatchSize: 100
MaximumBatchingWindowInSeconds: 5
ParallelizationFactor: 5
BisectBatchOnFunctionError: true
MaximumRetryAttempts: 3
MaximumRecordAgeInSeconds: 3600
FunctionResponseTypes:
- ReportBatchItemFailures
DestinationConfig:
OnFailure:
Destination: !GetAtt FailureQueue.Arn
FilterCriteria:
Filters:
- Pattern: '{"eventName": ["INSERT"]}'CDK example
import { DynamoEventSource, SqsDlq } from 'aws-cdk-lib/aws-lambda-event-sources';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
const table = new dynamodb.Table(this, 'MyTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
});
fn.addEventSource(new DynamoEventSource(table, {
startingPosition: lambda.StartingPosition.TRIM_HORIZON,
batchSize: 100,
maxBatchingWindow: Duration.seconds(5),
parallelizationFactor: 5,
bisectBatchOnError: true,
retryAttempts: 3,
maxRecordAge: Duration.hours(1),
reportBatchItemFailures: true,
onFailure: new SqsDlq(dlq),
}));---
SNS subscriptions
SNS invokes Lambda asynchronously — it is a direct trigger, NOT an event source mapping. No polling involved; SNS pushes events to Lambda.
Key characteristics
- Standard topics only (not FIFO)
- At-least-once delivery — make functions idempotent
- SNS retries at increasing intervals over several hours if Lambda is unreachable
- Cross-account subscriptions supported
Filter policies
Filter policies are managed by SNS (not Lambda FilterCriteria). Set FilterPolicyScope to control what is filtered:
| Scope | Filters on |
|---|---|
MessageAttributes (default) | SNS message attributes |
MessageBody | JSON body content |
{
"event_type": ["order_placed"],
"price_usd": [{"numeric": [">=", 100]}],
"store": [{"anything-but": "test_store"}]
}SAM template
ProcessorFunction:
Type: AWS::Serverless::Function
Properties:
Handler: processor.handler
Runtime: nodejs22.x
Events:
SNSEvent:
Type: SNS
Properties:
Topic: !Ref MyTopic
FilterPolicy:
event_type:
- order_placed
FilterPolicyScope: MessageAttributesCDK example
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
topic.addSubscription(new subscriptions.LambdaSubscription(fn, {
filterPolicy: {
event_type: sns.SubscriptionFilter.stringFilter({
allowlist: ['order_placed'],
}),
price: sns.SubscriptionFilter.numericFilter({
greaterThanOrEqualTo: 100,
}),
},
}));---
Event filtering
Lambda FilterCriteria applies to event source mappings only (not SNS or other push triggers).
Supported sources and filter keys
| Source | Filter key | Notes |
|---|---|---|
| SQS | body | Unmatched messages automatically deleted |
| DynamoDB Streams | dynamodb and metadata fields | Does NOT support numeric operators |
| Kinesis | data | Base64-decoded before filtering |
| MSK / Kafka | value | — |
| Amazon MQ | data | — |
Filter rules
- Up to 5 filters per ESM (can request increase to 10)
- Multiple filters are ORed — record matches if any filter matches
- Fields within a single filter are ANDed
Filter rule operators
| Operator | Syntax | Example |
|---|---|---|
| Equals | ["value"] | "City": ["Seattle"] |
| Equals (ignore case) | [{"equals-ignore-case": "value"}] | "City": [{"equals-ignore-case": "seattle"}] |
| Null | [null] | "UserID": [null] |
| Empty | [""] | "Name": [""] |
| Not | [{"anything-but": ["value"]}] | "Weather": [{"anything-but": ["Raining"]}] |
| Numeric equals | [{"numeric": ["=", 100]}] | "Price": [{"numeric": ["=", 100]}] |
| Numeric range | [{"numeric": [">", 10, "<=", 20]}] | "Price": [{"numeric": [">", 10, "<=", 20]}] |
| Exists | [{"exists": true}] | "Field": [{"exists": true}] |
| Prefix | [{"prefix": "us-"}] | "Region": [{"prefix": "us-"}] |
| Suffix | [{"suffix": ".png"}] | "FileName": [{"suffix": ".png"}] |
| Or (fields) | "$or": [{...}, {...}] | "$or": [{"City": ["NY"]}, {"Day": ["Mon"]}] |
DynamoDB filtering does NOT support numeric operators. Numbers are stored as strings in the DynamoDB JSON record.
Body/data format matching
| Incoming format | Filter format | Result |
|---|---|---|
| Plain string | Plain string | Filters normally |
| Plain string | Valid JSON | Lambda drops the message |
| Valid JSON | Plain string | Lambda drops the message |
| Valid JSON | Valid JSON | Filters normally |
Filter examples
# SQS — filter on body field
FilterCriteria:
Filters:
- Pattern: '{"body": {"RequestCode": ["BBBB"]}}'
# DynamoDB — INSERT events only
FilterCriteria:
Filters:
- Pattern: '{"eventName": ["INSERT"]}'
# DynamoDB — filter by NewImage attribute
FilterCriteria:
Filters:
- Pattern: '{"dynamodb": {"NewImage": {"status": {"S": ["ACTIVE"]}}}}'
# Kinesis — filter decoded data
FilterCriteria:
Filters:
- Pattern: '{"data": {"status": ["ACTIVE"]}}'---
Partial batch failure reporting
Enable by setting FunctionResponseTypes to ["ReportBatchItemFailures"].
SQS — return failed messageId values
export const handler = async (event) => {
const batchItemFailures = [];
for (const record of event.Records) {
try {
await processMessage(record);
} catch (error) {
batchItemFailures.push({ itemIdentifier: record.messageId });
}
}
return { batchItemFailures };
};Streams — return failed SequenceNumber values
For DynamoDB Streams and Kinesis, Lambda uses the lowest sequence number as the checkpoint and retries everything from that point.
export const handler = async (event) => {
for (const record of event.Records) {
try {
await processRecord(record);
} catch (e) {
return {
batchItemFailures: [
{ itemIdentifier: record.dynamodb.SequenceNumber },
// Kinesis: { itemIdentifier: record.kinesis.sequenceNumber }
],
};
}
}
return { batchItemFailures: [] };
};Python with Powertools Batch Processor
from aws_lambda_powertools.utilities.batch import (
BatchProcessor, EventType, process_partial_response,
)
processor = BatchProcessor(event_type=EventType.SQS)
def record_handler(record):
payload = record.body
# process payload...
def lambda_handler(event, context):
return process_partial_response(
event=event, record_handler=record_handler,
processor=processor, context=context,
)FIFO queue behavior
- Stop processing after the first failure
- Return all failed and unprocessed messages in
batchItemFailures - This preserves message ordering within the group
Success/failure conditions
| Response | Interpretation |
|---|---|
Empty batchItemFailures list | Complete success |
Null batchItemFailures or empty EventResponse | Complete success |
itemIdentifier is empty string or null | Complete failure (entire batch retried) |
Bad key name in itemIdentifier | Complete failure |
| Unhandled exception | Complete failure |
Interaction with BisectBatchOnFunctionError (streams)
- Function errors (unhandled exception):
BisectBatchOnFunctionErrorsplits the batch in half for retry.ReportBatchItemFailureshas no effect since no response was returned. - Function succeeds with
batchItemFailures: Lambda checkpoints at the lowest failed sequence number and retries from that point. IfBisectBatchOnFunctionErroris also enabled, the batch is bisected at the returned sequence number.
---
Error handling strategies
SQS
| Strategy | Configuration | When to use |
|---|---|---|
| SQS redrive policy (DLQ) | maxReceiveCount on the queue | Always — catches poison messages |
| Partial batch failures | ReportBatchItemFailures | Batches with mix of good/bad messages |
| Visibility timeout | Set to ≥ 6× function timeout | Always — prevents premature retry |
| MaximumConcurrency | ScalingConfig on ESM | Protect downstream resources |
DynamoDB Streams / Kinesis
| Strategy | Configuration | When to use |
|---|---|---|
| BisectBatchOnFunctionError | true | Isolate bad records in large batches |
| Partial batch failures | ReportBatchItemFailures | Avoid reprocessing successful records |
| Maximum retry attempts | MaximumRetryAttempts | Limit retries to prevent shard blocking |
| Maximum record age | MaximumRecordAgeInSeconds | Skip stale records |
| On-failure destination | DestinationConfig.OnFailure | Capture failed records for analysis |
| Parallelization factor | ParallelizationFactor | Reduce blast radius per shard |
ESM (polling) vs direct trigger (push)
| Aspect | ESM (SQS, DDB, Kinesis) | Async push (SNS, S3) | Sync push (API Gateway) |
|---|---|---|---|
| Invocation | Synchronous (Lambda polls) | Asynchronous (service pushes) | Synchronous (service pushes) |
| Batching | Yes (configurable) | No (single event) | No (single event) |
| Event filtering | Lambda FilterCriteria | SNS filter policies (SNS-managed) | N/A |
| Error handling | Partial batch, bisect, retry config | 2 automatic retries, DLQ/destination | Error returned directly to caller, no automatic retry |
| Ordering | Supported (streams, FIFO) | Not guaranteed | N/A (request/response) |
Concurrency formulas
SQS (default): min(1250, MaximumConcurrency, ReservedConcurrency)
SQS (provisioned): MaximumPollers × 10
DDB/Kinesis: number_of_shards × ParallelizationFactorIdempotency
All event sources deliver at least once — duplicates can occur. Use Powertools idempotency utility:
from aws_lambda_powertools.utilities.batch import (
BatchProcessor, EventType, process_partial_response,
)
from aws_lambda_powertools.utilities.idempotency import (
IdempotencyConfig, DynamoDBPersistenceLayer, idempotent_function,
)
processor = BatchProcessor(event_type=EventType.SQS)
persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
config = IdempotencyConfig(event_key_jmespath="messageId")
@idempotent_function(config=config, persistence_store=persistence_layer, data_keyword_argument="record")
def record_handler(record):
# process record...
pass
def lambda_handler(event, context):
return process_partial_response(
event=event, record_handler=record_handler,
processor=processor, context=context,
)AWS Lambda Reference
Specific values, limits, constraints, and code that complement general Lambda knowledge.
Contents
- Cold Start Optimization
- Packaging
- Memory and Timeout Tuning
- VPC Connectivity
- Execution Roles
- Runtime Lifecycle
- Powertools for AWS Lambda
---
Cold Start Optimization
SnapStart
Snapshots the initialized execution environment (Firecracker microVM memory + disk) and restores from cache instead of cold-booting.
Supported runtimes: Java 11+, Python 3.12+, .NET 8+ NOT supported: Node.js, Ruby, container images, OS-only runtimes
Constraints:
- Mutually exclusive with Provisioned Concurrency
- Mutually exclusive with Amazon EFS
- Ephemeral storage must be ≤ 512 MB
- Only works on published versions (not
$LATEST) - Java: no additional SnapStart overhead
- Python/.NET: caching charge (based on memory, minimum 3 hours) + per-restore charge
Restoration considerations:
- Generate unique IDs/secrets in the handler, not during init (snapshot reuse)
- Re-establish network connections in the handler (connections are stale after restore)
- Refresh cached timestamps/credentials in the handler
CDK example (Python):
from aws_cdk import aws_lambda as lambda_
fn = lambda_.Function(self, "MyFunction",
runtime=lambda_.Runtime.PYTHON_3_13,
handler="index.handler",
code=lambda_.Code.from_asset("lambda"),
snap_start=lambda_.SnapStartConf.ON_PUBLISHED_VERSIONS,
)
version = fn.current_versionProvisioned Concurrency
Pre-initializes execution environments that stay warm permanently.
- A single instance handles one concurrent request at a time; throughput per instance = 1 / function duration
- Account-level RPS quota: 10 × total concurrency (applies across all invocations, not per instance)
- Supports auto-scaling via Application Auto Scaling
- Lambda can scale beyond provisioned count using on-demand instances
- Paid even when idle — disable in dev/staging
const fn = new lambda.Function(this, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_22_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
});
const version = fn.currentVersion;
const alias = new lambda.Alias(this, 'ProdAlias', {
aliasName: 'prod',
version,
provisionedConcurrentExecutions: 10,
});Graviton (arm64)
- Up to 34% better price-performance compared to x86 (per AWS)
- Supported for all Lambda managed runtimes
- Set
architecture: lambda_.Architecture.ARM_64in CDK
Strategy Selection
| Scenario | Strategy |
|---|---|
| Java/Python/.NET with heavy init | SnapStart |
| Strict <50ms cold start | Provisioned Concurrency |
| Tolerant of occasional cold starts | On-demand + minimize package |
| Predictable traffic | Provisioned Concurrency + auto-scaling |
| General optimization | arm64 (Graviton) |
---
Packaging
Decision Tree
Need > 250 MB uncompressed?
└─ YES → Container image (up to 10 GB)
└─ NO
├─ Sharing deps across multiple functions?
│ └─ YES → Lambda layers
└─ NO
├─ Simple function, few deps → .zip
└─ Native binaries, complex build → Container imageSize Limits
| Package Type | Limit |
|---|---|
| .zip compressed | 50 MB |
| .zip uncompressed (including layers) | 250 MB |
| Container image | 10 GB |
| Layers per function | 5 |
Layer Paths by Runtime
| Runtime | Layer Path |
|---|---|
| Python | python/ or python/lib/python3.x/site-packages/ |
| Node.js | nodejs/node_modules/ |
| Java | java/lib/ |
| Ruby | ruby/gems/3.4.0/ or ruby/lib/ |
| All runtimes | bin/ (PATH), lib/ (LD_LIBRARY_PATH) |
Layer constraints:
- Layers count toward the 250 MB unzipped limit
- Layers only work with .zip deployments, NOT container images
- Not recommended for Go/Rust — bundle deps in the deployment package
- Multiple layers with conflicting dependency versions cause subtle bugs; merge order matters
Container Image Dockerfile
FROM public.ecr.aws/lambda/python:3.13
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py ${LAMBDA_TASK_ROOT}
CMD ["app.handler"]- Use official AWS base images from
public.ecr.aws/lambda/ - Container images do NOT support Lambda layers
- SnapStart is NOT supported with container images
Python Build Tips
Use uv for dependency installation — 10-100x faster than pip:
uv pip install -r requirements.txt --target ./packageCross-platform build flags (when building on non-Linux):
pip install -r requirements.txt \
--target ./package \
--platform manylinux2014_x86_64 \
--only-binary=:all:Use manylinux2014_aarch64 for arm64. Exclude __pycache__, .pyc, tests, docs.
---
Memory and Timeout Tuning
Memory
| Parameter | Value |
|---|---|
| Minimum | 128 MB |
| Maximum | 10,240 MB (10 GB) |
| Increment | 1 MB |
| Default | 128 MB |
| 1 vCPU at | 1,769 MB |
| ~5.8 vCPUs at | 10,240 MB |
CPU scales linearly with memory. Doubling memory doubles CPU. Over-provisioning memory can improve performance — faster execution = less total duration.
Tuning process:
1. Start at 256–512 MB (128 MB only for trivial event routers) 2. Monitor Max Memory Used in CloudWatch REPORT lines 3. Use AWS Lambda Power Tuning (open-source Step Functions tool):
aws stepfunctions start-execution \
--state-machine-arn arn:aws:states:REGION:ACCOUNT:stateMachine:powerTuningStateMachine \
--input '{
"lambdaARN": "arn:aws:lambda:REGION:ACCOUNT:function:my-function",
"powerValues": [128, 256, 512, 1024, 1769, 3008],
"num": 50,
"payload": "{\"test\": true}"
}'Ephemeral Storage (/tmp)
| Parameter | Value |
|---|---|
| Minimum / Default | 512 MB |
| Maximum | 10,240 MB (10 GB) |
| Extra cost | Above 512 MB |
- Content persists across warm invocations (use as transient cache)
- Content is NOT cleared after invoke failures
- SnapStart requires ≤ 512 MB ephemeral storage
Timeout
| Parameter | Value |
|---|---|
| Minimum | 1 second |
| Maximum | 900 seconds (15 minutes) |
| Default | 3 seconds |
Critical integration limits:
- API Gateway REST API: 29s default (adjustable for Regional/private APIs since June 2024; edge-optimized remains 29s max)
- API Gateway HTTP API: 30-second hard limit
- SQS visibility timeout must be ≥ 6× function timeout (AWS recommendation)
Other Limits
| Resource | Limit |
|---|---|
| Environment variables (total) | 4 KB |
| Sync invocation payload (request/response) | 6 MB each |
| Async invocation payload | 1 MB |
| Streamed response | 200 MB (first 6 MB uncapped, then 2 MBps) |
| File descriptors | 1,024 |
| Processes/threads | 1,024 |
| Concurrent executions (default) | 1,000 per region (soft limit) |
| Scaling rate | 1,000 new environments every 10s per function |
| Function code storage (.zip) | 75 GB per region (soft limit) |
---
VPC Connectivity
Hyperplane ENI
Lambda uses Hyperplane Elastic Network Interfaces (shared, not per-function):
- Shared across functions using the same subnet + security group combination
- Each ENI supports 65,000 connections/ports
- First-time ENI creation: several minutes (function stays in
Pending) - ENIs reclaimed after 14 days of inactivity (function goes
Inactive) - Removing VPC config takes up to 20 minutes for ENI cleanup
- Default quota: 500 Hyperplane ENIs per VPC (Lambda-specific soft limit, can be increased). The broader VPC ENI service quota is 5,000 per region by default.
Internet Access Patterns
Lambda in a VPC NEVER gets a public IP, even in a public subnet.
Pattern 1: Private Subnet + NAT Gateway (most common)
Lambda → Private Subnet → Route Table → NAT Gateway → IGW → Internet- Deploy in each AZ for HA
Pattern 2: VPC Endpoints (for AWS services)
Lambda → Private Subnet → VPC Endpoint → AWS Service- Gateway endpoints: S3, DynamoDB
- Interface endpoints: STS, Secrets Manager, SQS, etc.
- Traffic stays on AWS network — lower latency
Pattern 3: IPv6 Egress-Only Internet Gateway
Lambda → Dual-Stack Subnet → Egress-Only IGW → Internet (IPv6)- Eliminates NAT Gateway for IPv6 traffic
- Requires dual-stack subnets and IPv6-capable endpoints
- Set
Ipv6AllowedForDualStack=truein function config
Required IAM Permissions
VPC-attached functions need AWSLambdaVPCAccessExecutionRole managed policy or equivalent EC2 network interface permissions.
Best Practices
- Reuse subnet + security group combos across functions to share ENIs
- Use multiple subnets across AZs for HA
- Prefer VPC endpoints over NAT Gateway for AWS service access
- Don't attach to VPC unless accessing private resources (RDS, ElastiCache, etc.)
---
Execution Roles
One execution role per function. Key Lambda-specific managed policies:
| Policy | Grants |
|---|---|
AWSLambdaBasicExecutionRole | CloudWatch Logs only |
AWSLambdaVPCAccessExecutionRole | VPC ENI management |
AWSLambdaDynamoDBExecutionRole | DynamoDB Streams |
AWSLambdaSQSQueueExecutionRole | SQS polling |
AWSLambdaKinesisExecutionRole | Kinesis Streams |
---
Runtime Lifecycle
Phases
┌─────────┐ ┌─────────┐ ┌──────────┐
│ INIT │───▶│ INVOKE │───▶│ SHUTDOWN │
│ │ │(repeat) │ │ │
└─────────┘ └─────────┘ └──────────┘Init Phase (3 sub-phases: extension init → runtime init → function init):
- On-demand timeout: 10 seconds
- Provisioned/SnapStart timeout: up to 15 minutes
- If init exceeds 10s on-demand, Lambda retries at first invocation using the function's configured timeout
Invoke Phase:
- Limited by function timeout (max 900s)
- Each environment handles one concurrent invocation at a time
Shutdown Phase:
- 0 ms (no extensions), 500 ms (internal only), 2,000 ms (external extensions)
- SIGKILL if extensions don't respond in time
Restore Phase (SnapStart only):
- Resumes from cached snapshot
- 10-second timeout for restore + after-restore hooks
Execution Environment Reuse (Warm Starts)
Objects initialized outside the handler persist across invocations:
- SDK clients, DB connections, cached data all survive
/tmpcontent persists (512 MB–10 GB)- Background processes resume on next invocation
- Workers have a maximum lease lifetime of ~14 hours (observed behavior, not a documented SLA — do not depend on this value)
- Environments terminated periodically for maintenance even under continuous load
Common pitfall: Global variables persist — stale DB connections, expired credentials, and leaked state across invocations cause subtle production bugs.
Extensions
- Internal: Run in the runtime process (APM agents)
- External: Separate processes alongside the runtime
- Use Extensions API and Telemetry API for lifecycle events, logs, metrics, traces
---
Powertools for AWS Lambda
Official AWS toolkit for Lambda best practices. Available for Python, TypeScript, Java, .NET.
Performance note: Powertools adds cold start overhead. Use selective imports when cold start matters:
# Instead of: from aws_lambda_powertools import Logger, Tracer, Metrics
# Import only what you need if cold start is critical
from aws_lambda_powertools import LoggerCore Utilities
| Utility | Purpose |
|---|---|
| Logger | Structured JSON logging with correlation IDs |
| Tracer | X-Ray tracing with decorators/middleware |
| Metrics | CloudWatch metrics via Embedded Metric Format (EMF) |
| Idempotency | Make handlers idempotent using DynamoDB |
| Batch Processing | Partial failure handling for SQS, Kinesis, DynamoDB Streams |
| Event Handler | Routing for API Gateway, ALB, Function URLs, AppSync |
| Parameters | Retrieve/cache SSM, Secrets Manager, AppConfig, DynamoDB values |
Environment Variables
| Variable | Purpose |
|---|---|
POWERTOOLS_SERVICE_NAME | Service name for logs, metrics, traces |
POWERTOOLS_METRICS_NAMESPACE | CloudWatch metrics namespace |
POWERTOOLS_LOG_LEVEL | Logging level (DEBUG, INFO, WARNING, ERROR) |
POWERTOOLS_TRACE_DISABLED | Disable tracing (useful for tests) |
POWERTOOLS_DEV | Dev mode (pretty-print JSON, verbose errors) |
Python: Logger + Tracer + Metrics
from aws_lambda_powertools import Logger, Tracer, Metrics
from aws_lambda_powertools.metrics import MetricUnit
from aws_lambda_powertools.utilities.typing import LambdaContext
logger = Logger()
tracer = Tracer()
metrics = Metrics()
@logger.inject_lambda_context(log_event=False)
@tracer.capture_lambda_handler
@metrics.log_metrics(capture_cold_start_metric=True)
def handler(event: dict, context: LambdaContext) -> dict:
logger.info("Processing order", order_id=event.get("order_id"))
metrics.add_metric(name="OrdersProcessed", unit=MetricUnit.Count, value=1)
result = process_order(event)
return {"statusCode": 200, "body": result}
@tracer.capture_method
def process_order(event: dict) -> str:
return "processed"TypeScript: Logger + Tracer + Metrics
import { Logger } from '@aws-lambda-powertools/logger';
import { Tracer } from '@aws-lambda-powertools/tracer';
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
import middy from '@middy/core';
import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware';
import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware';
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware';
const logger = new Logger({ serviceName: 'orderService' });
const tracer = new Tracer({ serviceName: 'orderService' });
const metrics = new Metrics({ namespace: 'OrderApp', serviceName: 'orderService' });
const lambdaHandler = async (event: any) => {
logger.info('Processing order', { orderId: event.orderId });
metrics.addMetric('OrdersProcessed', MetricUnit.Count, 1);
const result = await processOrder(event);
return { statusCode: 200, body: JSON.stringify(result) };
};
export const handler = middy(lambdaHandler)
.use(injectLambdaContext(logger, { logEvent: false }))
.use(captureLambdaHandler(tracer))
.use(logMetrics(metrics, { captureColdStartMetric: true }));Python: Idempotency
from aws_lambda_powertools.utilities.idempotency import (
DynamoDBPersistenceLayer,
idempotent,
)
persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
@idempotent(persistence_store=persistence_layer)
def handler(event: dict, context) -> dict:
payment = process_payment(event)
return {"payment_id": payment.id, "status": "success"}TypeScript: Idempotency
import { makeIdempotent } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
const persistenceStore = new DynamoDBPersistenceLayer({
tableName: 'IdempotencyTable',
});
const processPayment = async (event: { paymentId: string; amount: number }) => {
return { paymentId: event.paymentId, status: 'success' };
};
export const handler = makeIdempotent(processPayment, {
persistenceStore,
});Python: Batch Processing (SQS Partial Failures)
from aws_lambda_powertools.utilities.batch import (
BatchProcessor,
EventType,
process_partial_response,
)
from aws_lambda_powertools.utilities.data_classes.sqs_event import SQSRecord
processor = BatchProcessor(event_type=EventType.SQS)
def record_handler(record: SQSRecord):
payload = record.json_body
process_item(payload)
def handler(event, context):
return process_partial_response(
event=event,
record_handler=record_handler,
processor=processor,
context=context,
)TypeScript: Batch Processing (SQS Partial Failures)
import {
BatchProcessor,
EventType,
processPartialResponse,
} from '@aws-lambda-powertools/batch';
import type { SQSRecord, SQSHandler } from 'aws-lambda';
const processor = new BatchProcessor(EventType.SQS);
const recordHandler = async (record: SQSRecord): Promise<void> => {
const payload = JSON.parse(record.body);
await processItem(payload);
};
export const handler: SQSHandler = async (event, context) => {
return processPartialResponse(event, recordHandler, processor, {
context,
});
};Asset Reference
For a ready-to-use Python handler with Powertools wired, read assets/powertools-handler.py.
Orchestration Reference
AWS Step Functions and Amazon EventBridge patterns and configuration.
Contents
- Step Functions Standard vs Express
- State machine patterns
- Error handling
- EventBridge rules and patterns
- EventBridge Pipes
---
Step Functions Standard vs Express
Decision Matrix
| Dimension | Standard | Express |
|---|---|---|
| Max duration | 1 year | 5 minutes |
| Execution semantics | Exactly-once | At-least-once (async) / At-most-once (sync) |
| Execution history | Stored 90 days (API/console) | CloudWatch Logs only (must enable) |
.sync integration | Supported | Not supported |
.waitForTaskToken | Supported | Not supported |
| Distributed Map | Supported | Not supported |
| Activities | Supported | Not supported |
| Idempotency | Automatic (execution name unique for 90 days) | Not managed |
Express sub-types:
- Asynchronous: Fire-and-forget. Results via CloudWatch Logs.
- Synchronous: Blocks until completion. Invokable from API Gateway, Lambda, or
StartSyncExecution. 5-min max.
| Use Case | Type |
|---|---|
Long-running orchestration, .sync/callback patterns | Standard |
| Non-idempotent operations (payments, exactly-once) | Standard |
| Distributed Map (large-scale parallel) | Standard |
| High-volume event processing (IoT, streaming) | Express |
| API-backed synchronous microservice orchestration | Synchronous Express |
---
State Machine Patterns
Saga Pattern (Compensating Transactions)
Each step has a corresponding undo step invoked on failure via Catch. Compensations chain in reverse.
{
"Comment": "Saga pattern — book travel",
"StartAt": "BookHotel",
"States": {
"BookHotel": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:book-hotel",
"TimeoutSeconds": 30,
"Catch": [{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.BookHotelError",
"Next": "NotifyFailure"
}],
"Next": "BookFlight"
},
"BookFlight": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:book-flight",
"TimeoutSeconds": 30,
"Catch": [{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.BookFlightError",
"Next": "CancelHotel"
}],
"Next": "BookCar"
},
"BookCar": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:book-car",
"TimeoutSeconds": 30,
"Catch": [{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.BookCarError",
"Next": "CancelFlight"
}],
"Next": "ConfirmBooking"
},
"CancelFlight": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:cancel-flight",
"Next": "CancelHotel"
},
"CancelHotel": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:cancel-hotel",
"Next": "NotifyFailure"
},
"NotifyFailure": {
"Type": "Fail",
"Error": "SagaFailed",
"Cause": "One or more bookings failed; compensations executed"
},
"ConfirmBooking": { "Type": "Succeed" }
}
}Parallel State
Executes branches concurrently. Output is an array with one element per branch. All branches must succeed or the entire Parallel state fails. Supports Retry and Catch.
{
"Type": "Parallel",
"Branches": [
{
"StartAt": "ProcessImages",
"States": {
"ProcessImages": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:process-images",
"End": true
}
}
},
{
"StartAt": "ProcessMetadata",
"States": {
"ProcessMetadata": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:process-metadata",
"End": true
}
}
}
],
"Next": "AggregateResults"
}Map State
Inline Map: Iterates over an array in the same execution. Max 40 concurrent iterations.
{
"Type": "Map",
"ItemsPath": "$.orders",
"MaxConcurrency": 10,
"ItemProcessor": {
"ProcessorConfig": { "Mode": "INLINE" },
"StartAt": "ProcessOrder",
"States": {
"ProcessOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:process-order",
"End": true
}
}
},
"Next": "Done"
}Distributed Map: Up to 10,000 parallel child executions. Reads from S3 (JSON, CSV, S3 inventory). Supports ItemBatcher, ItemReader, ResultWriter. Standard workflows only.
Choice State
Routes execution based on input conditions. Always include a Default branch.
Comparison operators: StringEquals, StringMatches, NumericGreaterThan, NumericLessThanEquals, BooleanEquals, IsPresent, IsNull, TimestampEquals, and Path variants.
{
"Type": "Choice",
"Choices": [
{ "Variable": "$.orderTotal", "NumericGreaterThan": 1000, "Next": "HighValueOrder" },
{ "Variable": "$.isPrime", "BooleanEquals": true, "Next": "PrimeProcessing" }
],
"Default": "StandardProcessing"
}Agentic AI Loop Pattern (Tool Use)
Model outputs a structured response indicating a tool call or final answer. Choice state routes accordingly. Tool results feed back in a loop.
{
"Comment": "Agentic AI loop with tool use",
"QueryLanguage": "JSONata",
"StartAt": "InvokeModel",
"States": {
"InvokeModel": {
"Type": "Task",
"Resource": "arn:aws:states:::bedrock:invokeModel",
"Arguments": {
"ModelId": "global.anthropic.claude-sonnet-4-6",
"Body": {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 4096,
"messages": "{% $states.input.messages %}"
},
"ContentType": "application/json",
"Accept": "application/json"
},
"Next": "CheckAction"
},
"CheckAction": {
"Type": "Choice",
"Choices": [
{ "Condition": "{% $states.input.Body.stop_reason = 'tool_use' %}", "Next": "ExecuteTool" }
],
"Default": "ReturnResult"
},
"ExecuteTool": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:execute-tool",
"TimeoutSeconds": 60,
"Next": "InvokeModel"
},
"ReturnResult": { "Type": "Succeed" }
}
}---
Error Handling
Built-in Error Names
| Error Name | Description | Retriable? |
|---|---|---|
States.ALL | Wildcard — matches any error | Yes |
States.TaskFailed | Wildcard for task errors (except States.Timeout) | Yes |
States.Timeout | Task exceeded TimeoutSeconds or HeartbeatSeconds | Yes |
States.HeartbeatTimeout | No heartbeat within HeartbeatSeconds | Yes |
States.Permissions | Insufficient IAM privileges | Yes |
States.DataLimitExceeded | Payload exceeds 256 KiB — terminal | No |
States.Runtime | Invalid JSONPath, null payload — terminal | No |
States.ItemReaderFailed | Map couldn't read from ItemReader source | Yes |
States.ResultWriterFailed | Map couldn't write to ResultWriter destination | Yes |
States.ALL does not match States.DataLimitExceeded or States.Runtime.
Retry Configuration
Available on Task, Parallel, and Map states. Retries are attempted before catchers.
"Retry": [
{
"ErrorEquals": ["States.Timeout"],
"IntervalSeconds": 3,
"MaxAttempts": 2,
"BackoffRate": 2.0,
"MaxDelaySeconds": 30,
"JitterStrategy": "FULL"
},
{
"ErrorEquals": ["Lambda.ServiceException", "Lambda.SdkClientException"],
"IntervalSeconds": 1,
"MaxAttempts": 3,
"BackoffRate": 2.0
},
{
"ErrorEquals": ["States.ALL"],
"IntervalSeconds": 1,
"MaxAttempts": 3,
"BackoffRate": 2.0
}
]| Field | Default | Description |
|---|---|---|
ErrorEquals | (required) | Array of error names to match |
IntervalSeconds | 1 | Initial wait before first retry |
MaxAttempts | 3 | Max retries; 0 = never retry |
BackoffRate | 2.0 | Multiplier for exponential backoff |
MaxDelaySeconds | — | Cap on computed backoff interval |
JitterStrategy | "NONE" | "FULL" randomizes wait between 0 and computed interval |
Rules:
States.ALLmust be last in the Retry array- Retries count as state transitions (billed in Standard workflows)
States.RuntimeandStates.DataLimitExceededcannot be retried- Use
JitterStrategy: "FULL"to prevent thundering herd
Catch (Fallback States)
"Catch": [
{
"ErrorEquals": ["CustomBusinessError"],
"ResultPath": "$.error-info",
"Next": "HandleBusinessError"
},
{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.error-info",
"Next": "GenericErrorHandler"
}
]ResultPathpreserves original input alongside the error (e.g.,"$.error-info")- Without
ResultPath, error output replaces entire input - Retries are attempted first; catchers apply only after retries are exhausted
Error handling best practices
1. Always set `TimeoutSeconds` on every Task state 2. Always retry Lambda service exceptions: Lambda.ServiceException, Lambda.SdkClientException 3. Use `HeartbeatSeconds` for long-running tasks 4. Combine Retry + Catch: Retry transient, Catch permanent 5. Use `JitterStrategy: "FULL"` to prevent thundering herd 6. Listen for execution failures via EventBridge for top-level failures
---
EventBridge Rules and Patterns
Event Pattern Structure
All specified fields must match (AND). Values within an array are OR'd.
{
"source": ["aws.ec2"],
"detail-type": ["EC2 Instance State-change Notification"],
"detail": { "state": ["terminated", "stopped"] }
}Advanced Pattern Operators
| Operator | Syntax | Description |
|---|---|---|
| Exact match | ["value"] | Field equals value |
| Prefix | [{"prefix": "prod-"}] | Starts with string |
| Suffix | [{"suffix": ".json"}] | Ends with string |
| Anything-but | [{"anything-but": ["val"]}] | Not in list |
| Numeric range | [{"numeric": [">", 0, "<=", 100]}] | Numeric comparison |
| Exists | [{"exists": true}] | Field must be present |
| Wildcard | [{"wildcard": "prod-*-east"}] | Glob-style matching |
EventBridge best practices
1. Dedicated event bus per application domain — default bus for AWS service events only 2. Be precise with patterns — broad patterns increase risk of infinite loops 3. One target per rule — simplifies debugging and IAM permissions 4. Use DLQs on targets — capture failed event deliveries 5. Use the EventBridge Sandbox to test patterns before deploying
Step Functions Status Change Events
Step Functions emits to the default bus automatically:
{
"source": ["aws.states"],
"detail-type": ["Step Functions Execution Status Change"],
"detail": { "status": ["FAILED", "TIMED_OUT", "ABORTED"] }
}Integration Patterns
SFN → EventBridge (publish events from a workflow):
{
"Type": "Task",
"QueryLanguage": "JSONata",
"Resource": "arn:aws:states:::events:putEvents",
"Arguments": {
"Entries": [{
"Detail": { "orderId": "{% $states.input.orderId %}", "status": "PROCESSED" },
"DetailType": "OrderProcessed",
"EventBusName": "my-app-bus",
"Source": "my-app.orders"
}]
},
"Next": "Done"
}EventBridge → SFN: Rule target is the state machine ARN. Event payload becomes execution input.
Fan-out: Single event triggers multiple workflows via multiple rules on the same bus.
---
EventBridge Pipes
Architecture
Source → [Filter] → [Enrichment] → [Transform] → TargetEliminates intermediary Lambda functions for point-to-point integrations.
Supported Sources
| Source | Notes |
|---|---|
| Amazon SQS | Standard and FIFO queues |
| Amazon Kinesis Data Streams | Shard-level polling |
| Amazon DynamoDB Streams | Change data capture |
| Amazon MSK / Self-managed Kafka | Topic-level consumption |
| Amazon MQ | ActiveMQ and RabbitMQ |
Enrichment Options
Lambda, API Gateway, EventBridge API Destinations, Step Functions (Synchronous Express).
Key Features
- Filtering: Event patterns filter at the source — pay only for matched events
- Ordering: Maintains event ordering within batches
- Built-in retry + DLQ: Source-level retry with dead-letter queue support
Pipes vs Rules
| Dimension | Pipes | Rules |
|---|---|---|
| Topology | Point-to-point (1→1) | Fan-out (1→N) |
| Sources | SQS, Kinesis, DDB Streams, MSK, MQ | Any event on a bus |
| Enrichment | Built-in | Not built-in |
| Use case | Replace Lambda glue | Event routing and distribution |
---
Lambda durable functions vs Step Functions
Lambda durable functions let you write reliable multi-step workflows as plain code (TypeScript, Python, Java) with automatic checkpointing — the SDK persists each step's result and replays from the checkpoint on interruption, enabling executions up to 1 year with zero compute during waits. Use the aws-lambda-durable-functions skill for full guidance.
| Question | Lambda durable functions | Step Functions |
|---|---|---|
| Primary focus? | Application logic in Lambda | Orchestration across AWS services |
| Programming model? | Standard code (TS/Python/Java) | Amazon States Language (ASL) or visual designer |
| AWS service integrations? | Primarily Lambda | 200+ native integrations |
| Who reads the workflow? | Developers | Non-technical stakeholders |
| Best for? | Distributed transactions, stateful logic, AI agent loops | Business process automation, multi-service orchestration |
Production-Ready Serverless on AWS
Quick-reference for shipping Lambda workloads to production. Covers the pre-deployment checklist, architecture trade-offs, and operational patterns for production traffic.
Contents
- Production readiness checklist
- Architecture decisions
- Observability
- Security hardening
- Testing strategies
- Idempotency patterns
- Response streaming
- Anti-patterns
---
Production readiness checklist
Walk through every item before the first production deployment.
Compute
- [ ] Memory right-sized (use AWS Lambda Power Tuning or load testing)
- [ ] Timeout set explicitly (P99 + buffer, never the 3 s default)
- [ ] Reserved concurrency configured to protect downstream systems
- [ ] Dead-letter queue (DLQ) or on-failure destination for every async invocation
- [ ] Environment variables for all config (bucket names, table names, endpoints)
- [ ] Code signing enabled (if compliance requires it)
- [ ] SDK clients initialized outside handler (reuse across warm invocations)
- [ ] Deployment package size minimized (exclude tests, docs, unused dependencies)
Observability
- [ ] Structured JSON logging via Powertools Logger
- [ ] X-Ray active tracing enabled
- [ ] Custom metrics emitted via Embedded Metric Format (EMF)
- [ ] CloudWatch Alarms on Errors, Throttles, Duration P99, IteratorAge, ConcurrentExecutions, DLQ depth
- [ ] Log retention policy set — do not leave at unlimited
- [ ] Correlation IDs propagated to downstream services
- [ ] Lambda Insights enabled for system-level metrics (CPU, memory, network)
Security
- [ ] One IAM execution role per function, scoped to exact resource ARNs
- [ ] No secrets in environment variables — use Secrets Manager / SSM with caching
- [ ] Input validation on every event payload (JSON Schema, Zod, Pydantic)
- [ ] VPC placement only when required (RDS, ElastiCache); VPC endpoints for AWS services
- [ ] GuardDuty Lambda Protection enabled
- [ ] Security Hub Lambda controls enabled
- [ ] Dependency scanning in CI (
npm audit,pip-audit, Snyk) - [ ] Amazon Inspector Lambda scanning enabled
- [ ] Function URLs use
AWS_IAMauth (notNONE) in production
Reliability
- [ ] Every handler is idempotent
- [ ] Partial batch failure reporting enabled (SQS, Kinesis, DynamoDB Streams)
- [ ]
BisectBatchOnFunctionErrorenabled for stream sources (isolates poison records) - [ ] Retry config tuned —
MaximumRetryAttempts,MaximumEventAgeInSeconds - [ ] Circuit breakers on downstream HTTP calls
- [ ] Reserved concurrency = 0 documented as emergency kill switch
- [ ] Graceful error handling — catch, log, and return meaningful errors (no unhandled exceptions)
Deployment
- [ ] Aliases + weighted traffic shifting (or CodeDeploy canary/linear)
- [ ] Rollback alarms wired into the deployment pipeline
- [ ] All infrastructure defined in code (CDK, SAM, or CloudFormation)
- [ ] Separate AWS accounts for dev, staging, production
- [ ] Automated smoke tests run post-deployment before full traffic shift
- [ ] Pre-traffic hooks (BeforeAllowTraffic) validate function health before shifting
---
Architecture decisions
Monolith Lambda vs micro-Lambda
| Aspect | Lambdalith (single function) | Micro-Lambda (function per route) |
|---|---|---|
| Cold starts | One function to warm; larger package | Many functions; smaller, faster init |
| IAM granularity | Single broad role | Per-function least-privilege |
| Deployment | Everything together; simpler CI/CD | Independent; more pipeline complexity |
| Observability | One log group; harder per-route metrics | Per-function metrics, alarms, logs |
| Scaling | Single concurrency pool | Independent scaling + reserved concurrency per function |
| DX | Familiar Express/FastAPI style | More AWS-native; requires IaC discipline |
Guidance: Prefer micro-Lambda for greenfield (least privilege, independent scaling, granular observability). Use Lambdalith when migrating existing Express/FastAPI apps or when team size makes deployment simplicity more valuable than granularity.
Function URLs vs API Gateway
| Feature | Function URLs | API Gateway (HTTP API) | API Gateway (REST API) |
|---|---|---|---|
| Auth | IAM only (or in-code) | IAM, JWT, Lambda authorizers | IAM, Cognito, Lambda authorizers, API keys |
| Rate limiting | None built-in | Built-in throttling | Throttling + usage plans |
| Response streaming | Yes (native) | No | Yes (proxy integration) |
| Custom domains | Via CloudFront | Built-in | Built-in |
| WAF | No (use CloudFront) | No (use CloudFront) | Yes |
| Request validation | None | None | JSON Schema |
| Caching | Via CloudFront | None | Built-in |
| WebSocket | No | No | No (separate WebSocket API required) |
Use Function URLs for: internal service-to-service (IAM auth), Lambdalith + CloudFront, streaming, webhook receivers.
Use API Gateway for: public APIs needing rate limiting, JWT/Cognito auth, multi-function path routing, WAF without CloudFront.
Reserved vs Provisioned Concurrency
| Aspect | Reserved Concurrency | Provisioned Concurrency |
|---|---|---|
| Purpose | Guarantee capacity + protect downstream | Eliminate cold starts |
| Cold starts | Still possible | Eliminated (pre-warmed) |
| Throttling | Throttles at the limit | Spills to on-demand beyond provisioned |
| Use case | Protect a database; guarantee capacity | Latency-sensitive APIs; payment processing |
Decision flow:
1. Need to limit scaling → Reserved concurrency 2. Need to eliminate cold starts → Provisioned concurrency (try SnapStart first — no additional cost for Java; caching + restore charges for Python/.NET) 3. Need both → Set provisioned ≤ reserved; reserved acts as the ceiling
---
Observability
Powertools setup (Python / TypeScript / Java / .NET)
Logger — structured JSON, correlation IDs injected automatically, log level via env var.
Tracer — wraps X-Ray SDK; auto-captures AWS SDK calls, HTTP requests, handler. Add custom subsegments for critical paths. Annotate traces with business keys (customer ID, order ID) for filtering.
Metrics — emits via Embedded Metric Format. Zero latency impact.
EMF vs PutMetricData
| EMF (Powertools Metrics) | PutMetricData API | |
|---|---|---|
| Latency impact | Zero — writes to stdout | Synchronous API call (~5–20 ms) |
| Complexity | One-liner with Powertools | Manual batching, error handling |
| Recommendation | Use this | Avoid in hot paths |
Minimum alarm set
Set these six alarms on every production function:
| Alarm | Metric | Threshold | Period | Why |
|---|---|---|---|---|
| Error rate | Errors / Invocations | > 1 % | 5 min | Catch bugs and upstream failures |
| Throttles | Throttles | > 0 | 5 min | Concurrency limit hit |
| Duration P99 | Duration P99 | > 80 % of timeout | 5 min | Catch slow functions before timeout |
| Iterator age | IteratorAge | > 60 s | 5 min | Stream processing falling behind |
| Concurrent executions | ConcurrentExecutions | > 80 % of reserved | 5 min | Approaching throttle threshold |
| DLQ depth | SQS ApproximateNumberOfMessagesVisible | > 0 | 5 min | Failed messages accumulating |
Log retention
Set retention when creating log groups. Defaults to "never expire" — storage accumulates continuously. Choose a retention period based on your compliance and debugging needs.
---
Security hardening
One role per function
Never share IAM roles across functions. Scope every policy to specific resource ARNs:
# Good
Effect: Allow
Action: dynamodb:PutItem
Resource: arn:aws:dynamodb:us-east-1:123456789012:table/OrdersTable
# Bad
Effect: Allow
Action: dynamodb:*
Resource: "*"Use IAM Access Analyzer to identify unused permissions and generate least-privilege policies.
Secrets management
- Store in Secrets Manager or SSM Parameter Store (SecureString)
- Cache in the execution environment with Powertools Parameters (avoids API call per invocation)
- Rotate automatically via Secrets Manager rotation Lambdas
- Environment variables are visible in the Lambda console and API — never put secrets there
Input validation
Validate at the handler boundary before business logic runs:
| Language | Library |
|---|---|
| TypeScript | Zod, io-ts, JSON Schema |
| Python | Pydantic, Powertools Validation (JSON Schema) |
| Java | Bean Validation (JSR 380), JSON Schema |
Powertools Validation supports envelope extraction for API Gateway, SQS, EventBridge, etc.
VPC: endpoints over NAT Gateway
If your function must be in a VPC, use VPC endpoints for AWS service access instead of NAT Gateway:
| VPC Endpoint | NAT Gateway | |
|---|---|---|
| Latency | Lower (stays on AWS backbone) | Higher (extra hop) |
Create endpoints for: DynamoDB (gateway), S3 (gateway), SQS, Secrets Manager, SSM, KMS.
---
Testing strategies
The serverless testing pyramid (inverted)
┌─────────────┐
│ E2E Tests │ Few — full workflow verification
├─────────────┤
│ Integration │ Many — THIS IS THE MOST VALUABLE LAYER
│ (in cloud) │ Test real service interactions
├─────────────┤
│ Unit Tests │ Fast — pure business logic only
└─────────────┘Serverless apps are primarily about service integrations, not complex business logic. Integration tests in the cloud detect the most impactful defects.
Structure code for testability
handler (thin adapter)
→ extract + validate event
→ call business logic (pure functions — unit test these)
→ call AWS services (integration test these in the cloud)What to test where
| Layer | What | How |
|---|---|---|
| Unit | Business logic (calculations, transforms, validation) | Local, fast, mocked dependencies |
| Integration | Service contracts (DynamoDB reads/writes, SQS send/receive, IAM permissions) | Deploy to AWS, test against real services |
| E2E | Full workflows (API → Lambda → DynamoDB → Stream → Lambda → SQS) | Dedicated staging environment; poll for async side effects |
Fast iteration
- `sam sync` — hot-deploys code changes to AWS in seconds
- `cdk watch` — watches for file changes and auto-deploys
- Each developer gets an isolated test stack (separate account or prefixed stack name)
What NOT to do
- Don't rely on LocalStack / DynamoDB Local as primary testing — they diverge from real AWS (IAM, quotas, error codes)
- Don't mock AWS SDK calls for integration tests — you'll miss permission and config issues
- Don't skip cloud testing because "it's slow" — use
sam sync/cdk watch
---
Idempotency patterns
Lambda guarantees at-least-once execution. Duplicates happen from: async retries, SQS visibility timeout expiry, stream shard replays, client retries on timeout, Step Functions task retries.
Powertools Idempotency utility
Uses DynamoDB to track processed events. Available for Python, TypeScript, Java, .NET.
Python:
from aws_lambda_powertools.utilities.idempotency import (
DynamoDBPersistenceLayer, idempotent
)
persistence = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
@idempotent(persistence_store=persistence)
def handler(event, context):
payment = process_payment(event)
return {"statusCode": 200, "body": payment}TypeScript:
import { makeIdempotent } from "@aws-lambda-powertools/idempotency";
import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/dynamodb";
const persistence = new DynamoDBPersistenceLayer({ tableName: "IdempotencyTable" });
export const handler = makeIdempotent(async (event) => {
const payment = await processPayment(event);
return { statusCode: 200, body: JSON.stringify(payment) };
}, { persistenceStore: persistence });DynamoDB table design
Table: IdempotencyTable
PK: id (String) — hash of the idempotency key
Attributes:
status: INPROGRESS | COMPLETED | EXPIRED
data: cached response payload
expiration: TTL epoch timestamp
TTL attribute: expirationChoosing the idempotency key
| Event source | Key |
|---|---|
| SQS | messageId |
| EventBridge | detail.id or composite of event fields |
| DynamoDB Streams | eventID |
| API Gateway / Function URL | Idempotency-Key header or request body hash |
| Step Functions | Execution ID + task token |
TTL for cleanup
Set TTL based on how long duplicates can arrive. Typical values:
- API retries: 1 hour
- SQS retries: match the queue's
maxReceiveCount× visibility timeout - Stream replays: 24 hours (Kinesis retention default)
DynamoDB automatically deletes expired items (typically within a few days of TTL expiry).
---
Response streaming
When to use
| Use case | Why streaming helps |
|---|---|
| Large payloads (> 6 MB) | Buffered limit is 6 MB; streaming supports up to 200 MB |
| TTFB-sensitive responses | Client sees partial data immediately (HTML shell, then content) |
| Server-sent events (SSE) | Real-time updates to browser clients |
| LLM / AI token streaming | Stream tokens as generated (conversational AI-style) |
| Large file generation | CSV/PDF rows streamed as produced |
Constraints
- Function URLs are simplest for streaming. REST API also supports streaming via proxy integration with STREAM transfer mode. HTTP API does not support streaming.
- 200 MB response limit
- 2 MBps bandwidth cap after the first 6 MB
- Billed for full function duration even if client disconnects
- Node.js has native support; other runtimes use custom runtime or Lambda Web Adapter
- Function URL streaming is NOT supported for VPC-attached functions. Use the
InvokeWithResponseStreamAPI as an alternative.
Node.js example
export const handler = awslambda.streamifyResponse(
async (event, responseStream, context) => {
const metadata = {
statusCode: 200,
headers: { "Content-Type": "text/html" },
};
responseStream = awslambda.HttpResponseStream.from(responseStream, metadata);
responseStream.write("<html><body>");
for (const chunk of generateContent()) {
responseStream.write(chunk);
}
responseStream.write("</body></html>");
responseStream.end();
}
);When NOT to use
- Small JSON responses (< 6 MB) — buffered is simpler
- When you need API Gateway features (rate limiting, caching, WAF) without CloudFront
- VPC-based functions needing Function URL streaming (use
InvokeWithResponseStreamAPI instead)
---
Sources
- AWS Lambda Best Practices
- Lambda Concurrency and Scaling
- Response Streaming
- How to Test Serverless Functions
- Serverless Applications Lens — Well-Architected
- Powertools for AWS Lambda
---
Anti-patterns
Common mistakes that cause production issues in serverless applications. Each pairs the problem with the correct alternative.
Avoid: Lambda calling Lambda synchronously
Synchronous Lambda-to-Lambda invocation doubles latency, creates tight coupling, and makes error handling fragile.
# BAD: Direct synchronous invocation
lambda_client.invoke(FunctionName='downstream', InvocationType='RequestResponse', Payload=json.dumps(event))Instead: Use Step Functions or SQS
# GOOD: Decouple via SQS
sqs.send_message(QueueUrl=QUEUE_URL, MessageBody=json.dumps(event))Or use Step Functions for orchestration when you need the result.
---
Avoid: Monolithic handler without intentional design
Routing logic stuffed into a single handler without considering trade-offs prevents independent scaling, broadens IAM blast radius, and increases cold start times.
# BAD: One function handling all routes without considering trade-offs
def handler(event, context):
path = event['path']
if path == '/users': return handle_users(event)
elif path == '/orders': return handle_orders(event)
elif path == '/products': return handle_products(event)Instead: Choose deliberately
For greenfield projects, prefer one function per route (least privilege, independent scaling, granular observability). For migrations from Express/FastAPI or small teams prioritizing deployment simplicity, a Lambdalith is a valid choice — see Architecture decisions for trade-offs.
---
Avoid: Secrets in environment variables
Visible in console and API, 4 KB total limit for all environment variables combined.
# BAD: Secret in env var
db_password = os.environ['DB_PASSWORD']Instead: Use Secrets Manager with Powertools caching
# GOOD: Cached secret retrieval
from aws_lambda_powertools.utilities import parameters
db_password = parameters.get_secret("my-db-secret", max_age=300)---
Avoid: Skipping idempotency
Lambda delivers at-least-once; duplicates cause duplicate records.
Instead: Use Powertools Idempotency
from aws_lambda_powertools.utilities.idempotency import idempotent, DynamoDBPersistenceLayer
persistence = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
@idempotent(persistence_store=persistence)
def handler(event, context):
return process_payment(event)---
Avoid: VPC when not needed
Adds cold start latency. Only attach Lambda to a VPC for private resources (RDS, ElastiCache, Elasticsearch). Use VPC endpoints for AWS service access instead.
---
Avoid: Default 3s timeout
Legitimate requests fail silently. Set timeout based on load-test P99 + buffer. Set SDK/HTTP client timeouts shorter than Lambda timeout to get meaningful errors instead of generic timeouts.
---
Avoid: Missing DLQ
Failed async invocations and event source messages are discarded without notification. Configure dead-letter queues on all async invocations and event source mappings.
---
Avoid: CloudWatch Logs retention = forever
Storage accumulates continuously. Set a retention period — do not leave at unlimited.
Related skills
How it compares
Pick aws-serverless over generic Lambda snippets when you need Powertools observability and DynamoDB idempotency in one Python handler template.
FAQ
Where do I start for a new serverless app?
Read architecture.md for pattern selection, then deployment.md for SAM or CDK templates.
What should I read first when debugging errors?
Start with troubleshooting.md and its five most common fixes before deeper references.
Does this skill cover Lambda Managed Instances?
No. Use the aws-lambda-managed-instances skill for LMI, capacity providers, and EC2-backed Lambda.
Is Aws Serverless safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.