AWS Lambda is the dominant serverless platform, but its patterns are specific: handler structure, context object, cold start optimization, event source differences (API Gateway vs SQS vs SNS vs EventBridge), and the correct way to handle partial batch failures. Claude Code generates Lambda handlers that account for these platform-specific requirements.
This guide covers AWS Lambda with Claude Code: handler patterns, event source integration, cold start optimization, and CDK deployment.
Lambda Handler Patterns
Create a Lambda function that handles API Gateway events.
It should: validate input, call a database, and return proper HTTP responses.
Include proper error handling and structured logging.
// src/handlers/api-handler.ts
import { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import { z } from 'zod';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb';
// Initialize outside handler — reused across warm invocations
const ddbClient = new DynamoDBClient({ region: process.env.AWS_REGION });
const ddb = DynamoDBDocumentClient.from(ddbClient);
const createOrderInput = z.object({
userId: z.string().uuid(),
items: z.array(z.object({
productId: z.string(),
quantity: z.number().int().positive(),
})).min(1),
});
export const handler: APIGatewayProxyHandlerV2 = async (event, context) => {
// Structured logging with request ID for tracing
const log = (level: string, msg: string, data?: object) =>
console[level](JSON.stringify({
level,
message: msg,
requestId: context.awsRequestId,
...data,
}));
log('info', 'Processing order request', {
path: event.rawPath,
method: event.requestContext.http.method,
});
try {
// Parse and validate body
const body = JSON.parse(event.body ?? '{}');
const input = createOrderInput.safeParse(body);
if (!input.success) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Validation failed',
details: input.error.flatten().fieldErrors,
}),
};
}
// Process order
const orderId = crypto.randomUUID();
await ddb.send(new PutCommand({
TableName: process.env.ORDERS_TABLE!,
Item: {
PK: `USER#${input.data.userId}`,
SK: `ORDER#${orderId}`,
orderId,
userId: input.data.userId,
items: input.data.items,
status: 'PENDING',
createdAt: new Date().toISOString(),
},
// Optimistic locking — prevent duplicate orders
ConditionExpression: 'attribute_not_exists(PK)',
}));
log('info', 'Order created', { orderId, userId: input.data.userId });
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json',
'X-Request-Id': context.awsRequestId,
},
body: JSON.stringify({ orderId, status: 'PENDING' }),
};
} catch (error) {
log('error', 'Order creation failed', {
error: (error as Error).message,
stack: (error as Error).stack,
});
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Internal server error' }),
};
}
};
CLAUDE.md for Lambda Projects
## AWS Lambda Conventions
- Runtime: Node.js 20.x
- SDK: AWS SDK v3 (@aws-sdk/* packages — tree-shakeable)
- Initialize clients OUTSIDE handler (reuse across invocations)
- Structured JSON logging — every log includes requestId
- Environment variables: ALL_CAPS, never hardcode ARNs
- Timeout: API handlers max 29s (API Gateway timeout), SQS handlers based on batch
- Memory: start at 256MB, tune based on CloudWatch metrics
- Deployment: AWS CDK (TypeScript)
- Local testing: AWS SAM (sam local invoke)
SQS Event Processing
Process SQS messages — each message is an order to fulfill.
Handle partial batch failures: some messages succeed, some fail.
Failed messages should go to DLQ, not block successful ones.
// src/handlers/sqs-processor.ts
import { SQSHandler, SQSBatchResponse } from 'aws-lambda';
interface OrderFulfillmentMessage {
orderId: string;
customerId: string;
items: Array<{ productId: string; quantity: number }>;
}
export const handler: SQSHandler = async (event): Promise<SQSBatchResponse> => {
const itemFailures: { itemIdentifier: string }[] = [];
// Process each message independently
await Promise.allSettled(
event.Records.map(async (record) => {
const messageId = record.messageId;
try {
const message = JSON.parse(record.body) as OrderFulfillmentMessage;
// Process the message
await fulfillOrder(message);
console.log(JSON.stringify({
level: 'info',
message: 'Order fulfilled',
orderId: message.orderId,
messageId,
}));
} catch (error) {
console.error(JSON.stringify({
level: 'error',
message: 'Order fulfillment failed',
messageId,
error: (error as Error).message,
}));
// Mark this message as failed — Lambda will send it to DLQ
itemFailures.push({ itemIdentifier: messageId });
}
})
);
// Return failed items — Lambda handles the rest
// Successfully processed messages are auto-deleted from the queue
return { batchItemFailures: itemFailures };
};
The batchItemFailures response tells SQS which specific messages failed — successful messages are deleted from the queue, failed ones are retried (up to maxReceiveCount, then go to DLQ).
Cold Start Optimization
My Lambda has 800ms cold starts. It's unacceptable for user-facing APIs.
What can I do?
Claude Code diagnoses common cold start causes and generates fixes:
1. Bundle size reduction (biggest impact):
// webpack.config.js — tree-shake AWS SDK
module.exports = {
target: 'node20',
mode: 'production',
// Mark AWS SDK as external — it's in Lambda runtime
externals: [
'@aws-sdk/client-dynamodb',
'@aws-sdk/client-s3',
// ... other SDK packages you use
],
};
// Or use esbuild (faster, better tree-shaking)
{
"scripts": {
"build": "esbuild src/handlers/*.ts --bundle --platform=node --target=node20 --outdir=dist --external:@aws-sdk/* --minify"
}
}
2. Lazy-load heavy dependencies:
// Don't import at module level
// import { someHeavyLibrary } from 'heavy-library'; // ❌ always loaded
export const handler = async (event) => {
// Import only when needed (cache in module scope after first load)
const { someHeavyLibrary } = await import('heavy-library'); // ✅ lazy loaded
};
3. Provisioned Concurrency for latency-sensitive functions:
// CDK — keep N instances warm
const fn = new lambda.Function(this, 'ApiHandler', { /* ... */ });
const alias = fn.addAlias('live');
alias.addAutoScaling({
minCapacity: 2, // Always warm
maxCapacity: 10,
});
4. Reduce initialization time:
// Initialize DB connection pool at module level (reused)
// But use lazy initialization for less-frequently-called functions
let dbPool: Pool | null = null;
function getDb(): Pool {
if (!dbPool) {
dbPool = new Pool({ connectionString: process.env.DATABASE_URL });
}
return dbPool;
}
CDK Deployment
Deploy this Lambda function with: API Gateway, an SQS queue backed by DLQ,
CloudWatch alarms, and proper IAM permissions.
// lib/order-processing-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigatewayv2';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
export class OrderProcessingStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// DLQ for failed messages
const dlq = new sqs.Queue(this, 'OrderFulfillmentDLQ', {
retentionPeriod: cdk.Duration.days(14),
});
// Main queue with DLQ
const queue = new sqs.Queue(this, 'OrderFulfillmentQueue', {
visibilityTimeout: cdk.Duration.seconds(60),
deadLetterQueue: {
queue: dlq,
maxReceiveCount: 3, // Retry 3 times before DLQ
},
});
// API handler Lambda
const apiHandler = new lambda.Function(this, 'ApiHandler', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'api-handler.handler',
code: lambda.Code.fromAsset('dist'),
timeout: cdk.Duration.seconds(29),
memorySize: 256,
environment: {
ORDERS_TABLE: 'Orders',
FULFILLMENT_QUEUE_URL: queue.queueUrl,
},
});
queue.grantSendMessages(apiHandler);
// SQS processor Lambda
const sqsProcessor = new lambda.Function(this, 'SqsProcessor', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'sqs-processor.handler',
code: lambda.Code.fromAsset('dist'),
timeout: cdk.Duration.seconds(60),
reservedConcurrentExecutions: 10, // Limit DB connections
});
sqsProcessor.addEventSource(new SqsEventSource(queue, {
batchSize: 10,
reportBatchItemFailures: true, // Enable partial failures
}));
// CloudWatch alarm for DLQ messages
new cloudwatch.Alarm(this, 'DlqAlarm', {
metric: dlq.metricNumberOfMessagesSent(),
threshold: 1,
evaluationPeriods: 1,
alarmDescription: 'Messages landed in fulfillment DLQ',
});
// HTTP API (cheaper than REST API)
const httpApi = new apigateway.HttpApi(this, 'OrderApi');
httpApi.addRoutes({
path: '/orders',
methods: [apigateway.HttpMethod.POST],
integration: new apigateway.integrations.HttpLambdaIntegration(
'CreateOrder', apiHandler
),
});
new cdk.CfnOutput(this, 'ApiEndpoint', {
value: httpApi.apiEndpoint,
});
}
}
For the broader serverless patterns beyond Lambda including Cloudflare Workers and Vercel Edge, see the serverless guide. For Infrastructure as Code using Terraform as an alternative to CDK, see the Terraform guide. The Claude Skills 360 bundle includes AWS Lambda skill sets for handler patterns, CDK constructs, and observability. Start with the free tier to try Lambda handler generation.