Lambda is AWS's flagship serverless compute. Beyond "deploy a function," real production Lambda usage has its own patterns — for cold starts, dependencies, configuration, error handling, and observability.
This page is about the patterns that hold up in production.
Lambda invokes a specific handler function:
def handler(event, context):
# event: input data (HTTP request, S3 event, etc.)
# context: Lambda runtime info
return { "statusCode": 200, "body": "ok" }
The shape varies by language and trigger source. Key patterns:
Anything at module-level runs once per cold start. Heavy initialization (database connections, ML models) at module level costs cold-start time but warms the function for subsequent invocations.
The trade-off: lazy initialization (in handler) makes cold starts faster but every cold start pays the cost.
# Module-level: runs once per cold start
db = initialize_db_connection()
def handler(event, context):
# Reuses db across warm invocations
return process(db, event)
For DB connections specifically, Lambda's connection model is awkward — see "RDS Proxy" below.
import os
TABLE_NAME = os.environ['TABLE_NAME']
Set environment variables in the Lambda configuration. Different per environment (dev/staging/prod). Don't put secrets here directly; reference Secrets Manager or Parameter Store instead.
Lambda invocations can be retried. Same SQS message delivered twice. Same SNS notification multiple times.
Always design Lambda handlers to be idempotent. Use idempotency keys, deduplication logic, or idempotent target operations (UPSERTs instead of INSERTs).
See IdempotencyPatterns.
Lambda is invoked by various sources:
Each trigger has a different event shape. Code your handler to match.
Memory: 128 MB - 10 GB. CPU scales linearly with memory; bigger Lambdas are faster, but cost more per ms.
Timeout: max 15 minutes. Tune for your workload.
For most workloads, 256-1024 MB is the sweet spot. Increase memory if CPU-bound; decrease if just orchestrating I/O.
Lambda layers package shared code/dependencies separately from handler code:
Function code (small)
+ Layer 1 (shared dependencies)
+ Layer 2 (more shared deps)
Useful for:
Trade-off: layers add complexity; for one-off functions, just include the code in the deployment package.
Connection pooling is hard with Lambda:
Solutions:
For production Lambda + RDS, RDS Proxy is essentially mandatory.
Lambda errors trigger automatic retries (for some triggers):
After retries are exhausted, the message goes to:
Always configure DLQs. Without them, failed invocations disappear silently.
CloudWatch is the default:
Enable X-Ray for non-trivial Lambdas. The trace shows time spent in your code vs. AWS service calls vs. cold start.
Structured JSON logs let CloudWatch Insights query them effectively. Avoid plain-text logs.
Per-invocation cost: $0.20 per million. Per-ms cost: depends on memory.
Common cost surprises:
For workloads with steady high traffic, ECS/Fargate is usually cheaper than Lambda.
Small, focused. Deployment via SAM, Serverless Framework, or AWS CDK.
Shared code, common deployment. Larger but easier to maintain consistency.
For infrastructure-as-code shops. Lambda is just another resource.
For most teams, a deployment framework (SAM, Serverless Framework, CDK) over raw IaC is more productive for Lambda-heavy work.