Back to Blog
AWSLambdaServerlessCloud

AWS Lambda Patterns for Production

Build production-ready Lambda functions. From cold starts to error handling to observability patterns.

B
Bootspring Team
Engineering
November 5, 2022
6 min read

Lambda functions need production patterns different from traditional servers. Here's how to build reliable, performant Lambda applications.

Basic Handler Pattern#

1import { APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda'; 2 3export const handler: APIGatewayProxyHandler = async (event) => { 4 try { 5 const body = JSON.parse(event.body || '{}'); 6 7 const result = await processRequest(body); 8 9 return { 10 statusCode: 200, 11 headers: { 12 'Content-Type': 'application/json', 13 'Access-Control-Allow-Origin': '*', 14 }, 15 body: JSON.stringify(result), 16 }; 17 } catch (error) { 18 console.error('Handler error:', error); 19 20 return { 21 statusCode: 500, 22 body: JSON.stringify({ error: 'Internal server error' }), 23 }; 24 } 25}; 26 27// Typed handler factory 28function createHandler<T, R>( 29 handler: (body: T, event: APIGatewayProxyEvent) => Promise<R>, 30 options: { statusCode?: number } = {} 31): APIGatewayProxyHandler { 32 return async (event) => { 33 try { 34 const body = JSON.parse(event.body || '{}') as T; 35 const result = await handler(body, event); 36 37 return { 38 statusCode: options.statusCode || 200, 39 headers: { 'Content-Type': 'application/json' }, 40 body: JSON.stringify(result), 41 }; 42 } catch (error) { 43 return handleError(error); 44 } 45 }; 46}

Cold Start Optimization#

1// Initialize outside handler - reused across invocations 2import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 3import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; 4 5// Connection reuse 6const client = new DynamoDBClient({}); 7const docClient = DynamoDBDocumentClient.from(client); 8 9// Lazy initialization 10let dbConnection: DatabaseConnection | null = null; 11 12async function getDbConnection(): Promise<DatabaseConnection> { 13 if (!dbConnection) { 14 dbConnection = await createConnection({ 15 host: process.env.DB_HOST, 16 // ... config 17 }); 18 } 19 return dbConnection; 20} 21 22export const handler: APIGatewayProxyHandler = async (event) => { 23 const db = await getDbConnection(); 24 // Use db... 25}; 26 27// Provisioned concurrency for critical functions 28// In serverless.yml: 29// functions: 30// api: 31// handler: handler.main 32// provisionedConcurrency: 5

Error Handling#

1// Custom error classes 2class AppError extends Error { 3 constructor( 4 message: string, 5 public statusCode: number, 6 public code: string 7 ) { 8 super(message); 9 this.name = 'AppError'; 10 } 11} 12 13class ValidationError extends AppError { 14 constructor(message: string, public details?: any) { 15 super(message, 400, 'VALIDATION_ERROR'); 16 } 17} 18 19class NotFoundError extends AppError { 20 constructor(resource: string) { 21 super(`${resource} not found`, 404, 'NOT_FOUND'); 22 } 23} 24 25// Error handler 26function handleError(error: unknown): APIGatewayProxyResult { 27 console.error('Error:', error); 28 29 if (error instanceof AppError) { 30 return { 31 statusCode: error.statusCode, 32 body: JSON.stringify({ 33 error: error.message, 34 code: error.code, 35 }), 36 }; 37 } 38 39 // Don't expose internal errors 40 return { 41 statusCode: 500, 42 body: JSON.stringify({ 43 error: 'Internal server error', 44 code: 'INTERNAL_ERROR', 45 }), 46 }; 47} 48 49// Middleware pattern 50const withErrorHandling = ( 51 handler: APIGatewayProxyHandler 52): APIGatewayProxyHandler => { 53 return async (event, context) => { 54 try { 55 return await handler(event, context, () => {}); 56 } catch (error) { 57 return handleError(error); 58 } 59 }; 60};

Request Validation#

1import { z } from 'zod'; 2 3const createUserSchema = z.object({ 4 email: z.string().email(), 5 name: z.string().min(1).max(100), 6 age: z.number().min(18).optional(), 7}); 8 9function validate<T>(schema: z.ZodSchema<T>, data: unknown): T { 10 const result = schema.safeParse(data); 11 12 if (!result.success) { 13 throw new ValidationError('Validation failed', result.error.flatten()); 14 } 15 16 return result.data; 17} 18 19export const createUser = createHandler(async (body) => { 20 const data = validate(createUserSchema, body); 21 return userService.create(data); 22}, { statusCode: 201 });

Middleware Chain#

1type Middleware = ( 2 handler: APIGatewayProxyHandler 3) => APIGatewayProxyHandler; 4 5// Auth middleware 6const withAuth: Middleware = (handler) => async (event, context, callback) => { 7 const token = event.headers.Authorization?.replace('Bearer ', ''); 8 9 if (!token) { 10 return { 11 statusCode: 401, 12 body: JSON.stringify({ error: 'Unauthorized' }), 13 }; 14 } 15 16 try { 17 const user = await verifyToken(token); 18 (event as any).user = user; 19 return handler(event, context, callback); 20 } catch { 21 return { 22 statusCode: 401, 23 body: JSON.stringify({ error: 'Invalid token' }), 24 }; 25 } 26}; 27 28// Logging middleware 29const withLogging: Middleware = (handler) => async (event, context, callback) => { 30 const start = Date.now(); 31 const requestId = context.awsRequestId; 32 33 console.log({ 34 type: 'REQUEST', 35 requestId, 36 path: event.path, 37 method: event.httpMethod, 38 }); 39 40 const result = await handler(event, context, callback); 41 42 console.log({ 43 type: 'RESPONSE', 44 requestId, 45 statusCode: result.statusCode, 46 duration: Date.now() - start, 47 }); 48 49 return result; 50}; 51 52// Compose middlewares 53const compose = (...middlewares: Middleware[]): Middleware => { 54 return (handler) => 55 middlewares.reduceRight((h, middleware) => middleware(h), handler); 56}; 57 58// Usage 59const enhance = compose(withErrorHandling, withLogging, withAuth); 60 61export const handler = enhance(async (event) => { 62 const user = (event as any).user; 63 return { 64 statusCode: 200, 65 body: JSON.stringify({ message: `Hello ${user.name}` }), 66 }; 67});

Async Processing#

1import { SQSHandler, SQSEvent } from 'aws-lambda'; 2 3// SQS handler with batch processing 4export const sqsHandler: SQSHandler = async (event: SQSEvent) => { 5 const results = await Promise.allSettled( 6 event.Records.map(async (record) => { 7 const body = JSON.parse(record.body); 8 await processMessage(body); 9 }) 10 ); 11 12 // Report failures for retry 13 const failures = results 14 .map((result, index) => ({ result, record: event.Records[index] })) 15 .filter(({ result }) => result.status === 'rejected') 16 .map(({ record }) => ({ 17 itemIdentifier: record.messageId, 18 })); 19 20 return { 21 batchItemFailures: failures, 22 }; 23}; 24 25// Invoke async function 26import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda'; 27 28const lambda = new LambdaClient({}); 29 30async function invokeAsync(functionName: string, payload: any): Promise<void> { 31 await lambda.send( 32 new InvokeCommand({ 33 FunctionName: functionName, 34 InvocationType: 'Event', // Async 35 Payload: JSON.stringify(payload), 36 }) 37 ); 38}

Observability#

1// Structured logging 2const logger = { 3 info: (message: string, data?: object) => { 4 console.log(JSON.stringify({ level: 'INFO', message, ...data })); 5 }, 6 error: (message: string, error: Error, data?: object) => { 7 console.error( 8 JSON.stringify({ 9 level: 'ERROR', 10 message, 11 error: error.message, 12 stack: error.stack, 13 ...data, 14 }) 15 ); 16 }, 17}; 18 19// X-Ray tracing 20import AWSXRay from 'aws-xray-sdk-core'; 21import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 22 23const client = AWSXRay.captureAWSv3Client(new DynamoDBClient({})); 24 25// Custom metrics 26import { MetricUnits, Metrics } from '@aws-lambda-powertools/metrics'; 27 28const metrics = new Metrics({ namespace: 'MyApp' }); 29 30export const handler = async (event) => { 31 metrics.addMetric('RequestCount', MetricUnits.Count, 1); 32 33 try { 34 const result = await processRequest(event); 35 metrics.addMetric('SuccessCount', MetricUnits.Count, 1); 36 return result; 37 } catch (error) { 38 metrics.addMetric('ErrorCount', MetricUnits.Count, 1); 39 throw error; 40 } finally { 41 metrics.publishStoredMetrics(); 42 } 43};

Configuration#

1// Use Parameter Store or Secrets Manager 2import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm'; 3 4const ssm = new SSMClient({}); 5const configCache: Record<string, string> = {}; 6 7async function getConfig(name: string): Promise<string> { 8 if (configCache[name]) { 9 return configCache[name]; 10 } 11 12 const result = await ssm.send( 13 new GetParameterCommand({ 14 Name: name, 15 WithDecryption: true, 16 }) 17 ); 18 19 const value = result.Parameter?.Value || ''; 20 configCache[name] = value; 21 return value; 22} 23 24// Environment-specific config 25const config = { 26 tableName: process.env.TABLE_NAME!, 27 region: process.env.AWS_REGION!, 28 stage: process.env.STAGE || 'dev', 29};

Best Practices#

Cold Starts: ✓ Initialize outside handler ✓ Use provisioned concurrency for critical paths ✓ Keep deployment packages small ✓ Avoid VPC when not needed Reliability: ✓ Implement proper error handling ✓ Use dead letter queues ✓ Set appropriate timeouts ✓ Implement retries with backoff Observability: ✓ Structured logging ✓ Custom metrics ✓ Distributed tracing ✓ Alerting on errors Security: ✓ Least privilege IAM ✓ Encrypt environment variables ✓ Validate all inputs ✓ Use secrets manager

Conclusion#

Production Lambda requires patterns for cold starts, error handling, and observability. Initialize connections outside handlers, implement proper middleware chains, and use structured logging. Monitor cold start times and consider provisioned concurrency for latency-sensitive functions.

Share this article

Help spread the word about Bootspring