AWS Lambda
Serverless compute. Upload code, AWS runs it on demand. No servers, no autoscaling config — you pay per invocation + execution time.
The model
def handler(event, context):
# event = JSON dict, source-dependent
# context = AWS Lambda metadata (request_id, deadline, ...)
return {"statusCode": 200, "body": json.dumps({"ok": True})}
Triggers: API Gateway, ALB, SQS, SNS, S3 events, EventBridge, DynamoDB Streams, Kinesis, direct invoke. Each gives a different event shape.
Concurrency model
Each invocation gets its own container. AWS spins up containers (“execution environments”) as concurrency demands. Idle containers are cached and reused for ~5-45 minutes (warm).
- Cold start — first invocation on a new container; pay the init cost (~100ms to several seconds depending on runtime + package size).
- Warm — subsequent invocations on a cached container; ~ms overhead.
# Module-level — runs ONCE per container (during cold start)
import boto3
import os
db_client = boto3.client("dynamodb") # reused across invocations
ENV = os.environ["ENV"]
def handler(event, context):
# Per-invocation
...
Init code is your friend — reuse SDK clients, pre-build expensive objects.
Cold start mitigations
| Strategy | Effect |
|---|---|
| Smaller package | faster cold start (less unzip) |
| Lower memory? No | actually MORE memory = more CPU = faster init |
| Provisioned Concurrency | keeps N containers warm 24/7 (~$$$) |
| SnapStart (Java/Python/.NET) | snapshots a warm container; restores in ~ms |
| Stay out of VPC unless needed | VPC ENI attach used to add 10s; now ~ms but extra path |
| Lighter runtime | Python 3.14 boots faster than e.g. heavy Java JVM |
| Lazy imports | only import the SDK module you need |
# Lazy imports — only pay the import cost in the invocations that need it
def handler(event, context):
if event.get("send_email"):
from email_module import send # only loaded when needed
send(event["to"], event["body"])
Memory + CPU
Memory is the single tuning knob; CPU and network scale linearly with memory.
| Memory | Approx CPU |
|---|---|
| 128 MB | 0.1 vCPU |
| 1769 MB | 1 full vCPU |
| 3008 MB | 2 vCPUs |
| up to 10240 MB | 6 vCPUs |
A CPU-bound function at 1769 MB is often cheaper than at 512 MB because it finishes in ~1/4 the time. Profile, don’t guess.
Timeouts
- Default: 3s. Max: 15 minutes (900s).
- For HTTP integrations via API Gateway: API Gateway has its own 29s timeout (REST) — Lambda may run longer but the HTTP request times out.
Triggers and event shapes
API Gateway / ALB
def handler(event, context):
body = json.loads(event.get("body") or "{}")
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"received": body}),
}
Use Mangum to run FastAPI / Starlette as a Lambda handler:
from fastapi import FastAPI
from mangum import Mangum
app = FastAPI()
@app.get("/items/{id}")
async def get_item(id: int):
return {"id": id}
handler = Mangum(app)
SQS
Lambda polls SQS; calls your function with up to N messages per batch (batchSize).
def handler(event, context):
for record in event["Records"]:
body = json.loads(record["body"])
process(body)
# If you raise, Lambda re-queues ALL messages in the batch
Use batchItemFailures to fail individual messages without re-queueing the whole batch:
def handler(event, context):
failed = []
for record in event["Records"]:
try:
process(json.loads(record["body"]))
except Exception:
failed.append({"itemIdentifier": record["messageId"]})
return {"batchItemFailures": failed}
DynamoDB Streams / Kinesis
Ordered processing per partition / shard; failed batches halt that shard. Use BisectBatchOnFunctionError: true to find the bad record without blocking forever.
VPC
By default, Lambda runs outside your VPC and can reach the public internet. VPC-attached Lambda can reach private subnets (RDS, ElastiCache) but:
- Cold start adds ~ms (used to be 10s; AWS fixed this in 2019 with Hyperplane ENIs).
- Egress to the internet requires NAT Gateway in your VPC.
- Use VPC Endpoints for AWS service access without NAT egress costs.
VpcConfig:
SubnetIds: [subnet-a, subnet-b]
SecurityGroupIds: [sg-lambda]
Lambda + RDS — the connection problem
Lambdas scale by spawning concurrent containers. Each one opens DB connections. Without coordination, 1000 concurrent lambdas → 1000 PostgreSQL connections → DB dies.
Fix: RDS Proxy. Lambda connects to the proxy; proxy multiplexes a small pool of DB connections. Failover transparent.
import psycopg
def handler(event, context):
with psycopg.connect(
host="orders-proxy.proxy-xxx.us-east-1.rds.amazonaws.com",
...
) as conn:
...
For DynamoDB you don’t have this problem — DDB has no concept of connections.
Concurrency controls
- Reserved concurrency — guarantees N concurrent executions; also caps at N (back-pressure).
- Provisioned concurrency — pre-warmed containers, always ready (no cold start), costs money 24/7.
- Unreserved (default) — shares the account-wide pool (1000 by default, raise by support ticket).
If a single hot Lambda hogs all 1000, every other Lambda in the account 429s. Reserve concurrency per function for isolation.
Idempotency
At-least-once delivery from SQS / Kinesis / DDB Streams = your function may run multiple times. Make it idempotent:
from aws_lambda_powertools.utilities.idempotency import idempotent
@idempotent(persistence_store=DynamoDBPersistenceLayer(table_name="idempotency"))
def handler(event, context):
...
Powertools stores (idempotency_key, result) in DynamoDB; same key → returns stored result, doesn’t re-run.
Layers
Shared code / dependencies across functions. Up to 5 layers per function, 250MB unzipped total.
Common use: heavy dependencies (pandas, numpy) — one layer shared across many functions, smaller per-function deployment packages, faster cold starts (less code to unzip).
Container images
Beyond zip files, Lambda supports OCI container images up to 10GB. Useful for:
- Heavy native dependencies (ML models, geospatial libs).
- Existing Docker workflows.
Cold start of container Lambda is comparable to zip if the base image is the AWS-provided one.
Power Tools for Lambda (Python)
aws-lambda-powertools library — tracing, metrics, structured logging, idempotency, batch processing, parameter store. Use it; it’s the de-facto stdlib for AWS Python Lambda.
from aws_lambda_powertools import Logger, Tracer, Metrics
logger = Logger()
tracer = Tracer()
metrics = Metrics()
@logger.inject_lambda_context
@tracer.capture_lambda_handler
@metrics.log_metrics
def handler(event, context):
logger.info("Processing", extra={"request_id": event["request_id"]})
metrics.add_metric(name="OrdersProcessed", unit="Count", value=1)
return {...}
Cost
- Pay per invocation ($0.20 per million).
- Pay per GB-second of memory ($0.0000166667 per GB-sec).
- First million requests + 400k GB-sec free per month (free tier).
example: 10M invocations/month, 256 MB, 200 ms avg
invocations: 10M × $0.20/M = $2.00
duration: 10M × 0.2s × 0.25 GB × $0.0000166667 = $8.33
total ≈ $10/month
ARM (Graviton2) is ~20% cheaper than x86, same performance for most workloads. Architecture: arm64.
When NOT to use Lambda
- Long-running (> 15 min) — use Fargate / Step Functions / EC2.
- Steady high load — at sustained traffic, ECS / Fargate is cheaper than Lambda’s per-invocation pricing.
- Latency-sensitive with cold-start aversion — use Provisioned Concurrency or move to a always-on container.
- WebSocket with very long sessions — use API Gateway WebSocket but the connection lives in API Gateway, not in your Lambda.
Interview angle
- “What is a cold start and how do you mitigate it?” — first invocation on a new execution environment incurs the runtime init + your top-level code. Mitigate with: smaller packages, lazy imports, more memory (more CPU), Provisioned Concurrency, SnapStart, staying out of VPC unless needed.
- “How do you connect a Lambda to RDS without exhausting connections?” — RDS Proxy. Lambdas connect to the proxy; it multiplexes thousands of client connections to a small DB pool, handles failover. Direct Lambda → RDS at scale is a classic outage shape.
- “Reserved vs Provisioned Concurrency?” — Reserved: guarantees + caps concurrency for a function (back-pressure + isolation). Provisioned: pre-warms N execution environments to eliminate cold starts; costs money even when idle.
- “How does Lambda handle SQS failures?” — by default, raising in your handler returns the whole batch to the queue, eventually DLQ via SQS
maxReceiveCount. UsebatchItemFailuresresponse to fail individual messages while keeping the rest. - “Lambda is at-least-once. How do you make it exactly-once?” — make the function idempotent. Use Powertools’ idempotency decorator (stores key + result in DDB) or your own dedupe layer.
- “When wouldn’t you use Lambda?” — long-running (>15min), steady high-volume (Fargate cheaper), latency-sensitive without willingness to pay for Provisioned Concurrency, or workloads needing local file storage / persistent in-memory state.