AWS Lambda Best Practices for Production Workloads
Running AWS Lambda in production requires more than just uploading code and connecting an API Gateway. Teams that treat Lambda as a simple "function runner" often hit performance walls, debugging nightmares, and unexpectedly high bills. This guide covers the practices that separate production-grade Lambda deployments from quick prototypes.
Cold Start Optimization
Cold starts remain the most discussed Lambda pain point. When AWS provisions a new execution environment, your function must download its code, initialize the runtime, and execute your handler's initialization code before processing the first request.
To minimize cold start impact:
- Keep deployment packages small. Strip unused dependencies. For Node.js, use bundlers like esbuild. For Python, avoid including test frameworks or development tools.
- Prefer ARM64 (Graviton2). ARM-based Lambda functions consistently show lower cold start times alongside a 20% cost reduction.
- Move initialization outside the handler. Database connections, SDK clients, and configuration loading should happen at module scope, not inside the handler function.
import boto3
import os
# Initialized once per execution environment
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])
def handler(event, context):
# Reuses the connection from above
response = table.get_item(Key={'id': event['id']})
return response['Item']
Memory Allocation Tuning
Lambda allocates CPU proportionally to memory. A 1,769 MB function gets one full vCPU. This means increasing memory often makes functions faster and cheaper since they complete in less time.
Use AWS Lambda Power Tuning, an open-source tool that runs your function at different memory settings and charts the cost-performance tradeoff. The results frequently surprise teams: a function at 512 MB might cost more than the same function at 1,024 MB because it runs three times longer.
A practical approach:
- Start with 256 MB for I/O-bound functions and 512 MB for CPU-bound ones.
- Run power tuning with a representative payload.
- Pick the memory setting at the "knee" of the cost curve.
- Revisit after significant code changes.
Lambda Layers
Layers let you share common dependencies across functions without duplicating them in every deployment package. This reduces package sizes and simplifies dependency management.
# Create a layer with shared utilities
mkdir -p python/lib/python3.12/site-packages
pip install requests boto3-stubs -t python/lib/python3.12/site-packages
zip -r shared-layer.zip python
aws lambda publish-layer-version \
--layer-name shared-utilities \
--zip-file fileb://shared-layer.zip \
--compatible-runtimes python3.12
Keep layers focused. A "kitchen sink" layer that bundles everything defeats the purpose. Aim for logical groupings: one layer for database utilities, another for observability tooling.
Error Handling and Retry Strategies
Lambda's retry behavior depends on the invocation type:
- Synchronous (API Gateway): No automatic retries. Your code must handle errors and return appropriate HTTP status codes.
- Asynchronous (S3, SNS): Lambda retries twice with delays. Configure a dead-letter queue (DLQ) or on-failure destination to catch persistent failures.
- Stream-based (Kinesis, DynamoDB Streams): Lambda retries until the record expires. Use
bisectBatchOnFunctionErrorandmaxRetryAttemptsto prevent poison pills from blocking an entire shard.
// CDK configuration for a Kinesis-triggered Lambda
const fn = new lambda.Function(this, 'Processor', { /* ... */ });
fn.addEventSource(new KinesisEventSource(stream, {
startingPosition: lambda.StartingPosition.TRIM_HORIZON,
batchSize: 100,
bisectBatchOnFunctionError: true,
maxRetryAttempts: 3,
onFailure: new SqsDestination(dlq),
retryAttempts: 3,
}));
Structured Logging
Unstructured log lines become impossible to query at scale. Use structured JSON logging from day one.
from aws_lambda_powertools import Logger
logger = Logger(service="payment-service")
@logger.inject_lambda_context
def handler(event, context):
logger.info("Processing payment", extra={
"order_id": event["order_id"],
"amount": event["amount"],
"currency": event["currency"]
})
AWS Lambda Powertools (available for Python, TypeScript, Java, and .NET) provides structured logging, tracing, and metrics with minimal boilerplate. CloudWatch Logs Insights can then query JSON fields directly:
fields @timestamp, order_id, amount
| filter service = "payment-service"
| filter amount > 1000
| sort @timestamp desc
VPC Considerations
Placing Lambda in a VPC adds network interface creation to cold starts. AWS has improved this dramatically with Hyperplane ENIs, but it still adds latency and requires careful subnet planning.
Only attach Lambda to a VPC when you genuinely need to access private resources like RDS instances or ElastiCache clusters. For DynamoDB and S3, use VPC gateway endpoints instead. For other AWS services, use VPC interface endpoints to avoid routing through the public internet while keeping Lambda outside the VPC.
When VPC attachment is necessary:
- Provision subnets in multiple Availability Zones.
- Size your subnets appropriately. Each Lambda execution environment consumes an IP address from the subnet.
- Use security groups to restrict egress to only the resources your function needs.
Provisioned Concurrency
For latency-sensitive workloads that cannot tolerate cold starts, provisioned concurrency keeps a specified number of execution environments warm and initialized.
aws lambda put-provisioned-concurrency-config \
--function-name payment-processor \
--qualifier prod \
--provisioned-concurrent-executions 50
Provisioned concurrency costs money whether those environments handle requests or not. Combine it with Application Auto Scaling to adjust based on utilization:
- Set a baseline provisioned concurrency for expected steady-state traffic.
- Configure auto-scaling to ramp up before known peak periods.
- Use scheduled scaling for predictable traffic patterns like business hours.
Monitor the ProvisionedConcurrencySpilloverInvocations metric to identify when traffic exceeds your provisioned capacity. A sustained spillover rate signals the need to increase your provisioned baseline or review your scaling policy.
Putting It All Together
Production Lambda functions work best when teams treat them as components in a larger system rather than isolated scripts. Set up structured logging before you need to debug an incident. Configure DLQs before a poison pill blocks your event pipeline. Tune memory allocation before costs surprise you in the monthly bill. These practices compound: a well-instrumented, properly-sized, correctly-configured function is easier to operate and costs less than one bolted together under pressure.