AWS Lambda

Updated 6 min read index source
On this page16
  1. The model
  2. Concurrency model
  3. Cold start mitigations
  4. Memory + CPU
  5. Timeouts
  6. Triggers and event shapes
  7. VPC
  8. Lambda + RDS — the connection problem
  9. Concurrency controls
  10. Idempotency
  11. Layers
  12. Container images
  13. Power Tools for Lambda (Python)
  14. Cost
  15. When NOT to use Lambda
  16. Interview angle

AWS Lambda

Serverless compute. Upload code, AWS runs it on demand. No servers, no autoscaling config — you pay per invocation + execution time.

The model

python
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.
python
# Module-level — runs ONCE per container (during cold start)
import boto3
import os
# reused across invocations
db_client = boto3.client("dynamodb")
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
python
# Lazy imports — only pay the import cost in the invocations that need it
def handler(event, context):
    if event.get("send_email"):
        # only loaded when needed
        from email_module import send
        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

python
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:

python
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).

python
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:

python
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.
yaml
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.

python
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:

python
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.

python
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).
text
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 6

  • “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. Use batchItemFailures response 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.

Contents 0