Production-Ready Serverless on AWS
Pre-deployment checklist, architecture trade-offs, and operational patterns. Concise on purpose — the value here is the structured checklist and opinionated defaults, not API syntax.
Contents
- Production readiness checklist
- Architecture decisions
- Observability
- Idempotency
- Response streaming
- Anti-patterns
Production readiness checklist
Compute
- Memory right-sized (Power Tuning / load test)
- Timeout set explicitly (P99 + buffer — never the 3s default)
- Reserved concurrency set to protect downstream
- DLQ / on-failure destination on every async invocation and ESM
- Config via env vars; SDK clients initialized outside the handler
- Deployment package minimized (exclude tests, docs, unused deps)
Observability
- Structured JSON logging (Powertools Logger) + correlation IDs propagated
- X-Ray active tracing
- Custom metrics via EMF
- Alarms on Errors, Throttles, Duration P99, IteratorAge, ConcurrentExecutions, DLQ depth (see below)
- Log retention set — not unlimited
- Log group encryption with customer managed KMS key when compliance requires it
- Lambda Insights enabled
Security
- One IAM role per function, scoped to exact resource ARNs
- No secrets in env vars — Secrets Manager / SSM (SecureString) with Powertools caching
- Input validation at the handler boundary (Zod / Pydantic / Powertools Validation)
- VPC only when required (RDS, ElastiCache); VPC endpoints for AWS services
- GuardDuty Lambda Protection, Security Hub Lambda controls, Inspector Lambda scanning, CI dependency scanning
- Function URLs use
AWS_IAMauth (notNONE) in production - SQS queues use SSE-KMS for encryption at rest when compliance requires customer managed keys (SSE-SQS is enabled by default)
- DynamoDB tables use customer managed KMS keys when compliance requires key control (AWS owned encryption is enabled by default)
- Enforce HTTPS-only access with
aws:SecureTransportcondition in resource policies (S3 buckets, SQS queues)
Reliability
- Every handler idempotent
- Partial batch failure reporting (SQS, Kinesis, DynamoDB Streams)
-
BisectBatchOnFunctionErrorfor stream sources - Retry config tuned (
MaximumRetryAttempts,MaximumEventAgeInSeconds) - Reserved concurrency = 0 documented as the emergency kill switch
- No unhandled exceptions — catch, log, return meaningful errors
Deployment
- Aliases + weighted shifting (or CodeDeploy canary/linear) with rollback alarms
- All infra in code (CDK/SAM/CloudFormation)
- Separate accounts for dev/staging/prod
- Post-deploy smoke tests + pre-traffic hooks before full shift
Architecture decisions
Lambdalith vs micro-Lambda
Prefer micro-Lambda (function per route) for greenfield: per-function least-privilege IAM, independent scaling + reserved concurrency, granular observability, smaller/faster cold starts. Use a Lambdalith when migrating an existing FastAPI app or when a small team values deployment simplicity over granularity. For Node.js apps, Lambda Web Functions runs the HTTP server directly without an adapter — route to the aws-lambda-web-functions skill; the Lambdalith adapter pattern applies to Python/FastAPI.
Reserved vs Provisioned Concurrency
Reserved = guarantee capacity + protect downstream (cold starts still possible, throttles at the limit). Provisioned = eliminate cold starts (spills to on-demand beyond the count). Need both → provisioned ≤ reserved. Try SnapStart before Provisioned for Java/Python 3.12+/.NET 8+ (no cost for Java). See concurrency.md.
Observability
Use Powertools Logger (structured JSON, auto correlation IDs), Tracer (wraps X-Ray, auto-captures SDK/HTTP calls; annotate traces with business keys), Metrics (EMF — zero latency, writes to stdout vs ~5–20ms for synchronous PutMetricData; avoid PutMetricData in hot paths).
Minimum alarm set (every production function)
| Alarm | Metric | Threshold | Why |
|---|---|---|---|
| Error rate | Errors / Invocations |
> 1% | Bugs / upstream failures |
| Throttles | Throttles |
> 0 | Concurrency limit hit |
| Duration P99 | Duration P99 |
> 80% of timeout | Catch slow functions before timeout |
| Iterator age | IteratorAge |
> 60s | Stream processing falling behind |
| Concurrent executions | ConcurrentExecutions |
> 80% of reserved | Approaching throttle threshold |
| DLQ depth | SQS ApproximateNumberOfMessagesVisible |
> 0 | Failed messages accumulating |
Set log retention when creating log groups — the default is "never expire."
Testing
Serverless apps are mostly about service integrations, not complex business logic — so the integration layer (tested in the cloud) is the most valuable, with few unit tests (pure logic) and few E2E. Structure handlers as thin adapters calling pure functions. Don't rely on LocalStack/DynamoDB Local as primary testing (they diverge on IAM, quotas, error codes); don't mock AWS SDK calls for integration tests. Iterate fast with sam sync / cdk watch; give each developer an isolated stack.
Idempotency
Lambda guarantees at-least-once — duplicates come from async retries, SQS visibility expiry, stream replays, client retries, Step Functions task retries. Use the Powertools Idempotency utility (Python/TS/Java/.NET), backed by a DynamoDB table with TTL.
Table: id (hash of idempotency key) + status (INPROGRESS/COMPLETED/EXPIRED), data (cached response), expiration (TTL).
Idempotency key by source:
| Source | Key |
|---|---|
| SQS | messageId |
| EventBridge | detail.id or composite |
| DynamoDB Streams | eventID |
| API Gateway / Function URL | Idempotency-Key header or body hash |
| Step Functions | Execution ID + task token |
TTL ≈ how long duplicates can arrive (API retries ~1h; SQS ≈ maxReceiveCount × visibility; stream replays ~24h).
Response streaming
Use for payloads > 6 MB (buffered limit), TTFB-sensitive responses, SSE, LLM token streaming, or large file generation.
- Function URLs are simplest; REST API also supports streaming (proxy, STREAM mode); HTTP API does not.
- 200 MB response limit; 2 MBps cap after the first 6 MB.
- Billed for full duration even if the client disconnects.
- Node.js has native support (
awslambda.streamifyResponse+awslambda.HttpResponseStream.from); other runtimes need a custom runtime or Lambda Web Adapter. - Not supported for VPC-attached functions via Function URL — use the
InvokeWithResponseStreamAPI instead.
Don't stream small JSON (< 6 MB) — buffered is simpler.
Anti-patterns
- Lambda calling Lambda synchronously — doubles latency, tight coupling, fragile error handling. Decouple via SQS, or use Step Functions when you need the result.
- Unintentional monolithic handler — routing stuffed into one function without weighing trade-offs prevents independent scaling and broadens IAM blast radius. Choose Lambdalith vs micro-Lambda deliberately (see above).
- Secrets in env vars — visible in console/API, 4 KB total cap. Use Secrets Manager with Powertools caching.
- Skipping idempotency — at-least-once delivery causes duplicate records.
- VPC when not needed — adds cold-start latency. Only for private resources; use VPC endpoints for AWS services.
- Default 3s timeout — legitimate requests fail silently. Set SDK/HTTP client timeouts shorter than the Lambda timeout for meaningful errors.
- Missing DLQ — failed async invocations / ESM messages are discarded silently.
- Log retention = forever — storage accumulates continuously.