The problem: p99 is 3 seconds, p50 is 40 ms
Your Lambda averages look fine in the dashboard. Customers still complain. You dig into traces and find a bimodal distribution: warm invokes are fast; cold starts (especially in a VPC) dominate p99. “Just add more memory” helps a little. The real levers are init path, concurrency, and networking.
What a cold start actually includes
- Download your deployment package / image layers.
- Start the execution environment (MicroVM).
- Runtime init + your static/global code.
- For VPC: attach Hyperplane ENIs (historically painful; improved but not free).
Global/module-level code runs on cold start. Opening DB connections, loading huge ORMs, or parsing megabyte JSON configs in the initializer is how you donate seconds to every scale-from-zero burst.
Provisioned concurrency vs reserved concurrency
- Reserved concurrency caps how many executes your function can run — protects downstream and prevents account-level throttling from one noisy function. Set it or a runaway SQS trigger will take the account’s burst concurrency.
- Provisioned concurrency keeps environments warm. You pay for readiness. Great for latency-critical APIs; wasteful for spiky batch if you over-provision 24/7.
Combine with Application Auto Scaling on provisioned concurrency when traffic is diurnal.
VPC Lambdas: the ENI story in 2026
AWS moved to Hyperplane-backed ENIs so every function in a subnet no longer needs a unique ENI per environment the way early VPC Lambda did. You still pay for:
- Cold starts that need network setup path
- NAT Gateway data processing if private subnets need internet (package repos, third-party APIs)
- Security group + subnet capacity planning
Prefer not putting Lambda in a VPC unless it must reach RFC1918 resources. Reach AWS APIs via public endpoints / VPC endpoints; reach the internet from non-VPC functions without NAT charges.
When you must use VPC: put interfaces in multiple AZs, use VPC endpoints for S3/Secrets Manager/STS to avoid NAT, and keep security groups tight.
Concurrency and downstream collapse
Lambda scales to thousands of concurrent executes faster than RDS or a third-party API. Classic failure:
- SQS queue depth grows overnight.
- Lambda scales to 500 concurrent consumers.
- Each opens DB connections → database refuses connections.
- Messages return to queue → more invokes → death spiral.
Controls: reserved concurrency on the consumer, RDS Proxy, batch size / maximum concurrency on the event source mapping, and idempotent handlers so retries are safe.
Package size and runtime choices
- Strip unused deps; prefer AWS SDK v3 modular packages.
- Container images help large ML deps but can worsen cold start if layers are fat — measure.
- SnapStart (Java) dramatically improves cold starts for compatible managed runtimes — validate serialization constraints.
- Arm64 (Graviton) often wins on price/perf; benchmark your workload.
Observability that finds the bimodal latency
- Emit a custom metric
ColdStart=1from init (or use reported Init Duration in CloudWatch Logs Insights). - Trace with X-Ray / OpenTelemetry — separate downstream HTTP from init.
- Alarm on concurrent executions, throttles, and p99 duration — not average duration.
Design patterns that work
- Keep handlers thin; initialize clients once in global scope carefully (reuse, but avoid secret work at import time that is not needed for all paths).
- Use SQS/SNS/EventBridge for async; API Gateway + Lambda for request/response with provisioned concurrency only where SLOs demand it.
- For VPC data access, consider moving the hot path to a service already in the VPC (ECS) if cold start SLOs cannot be met economically.
Cold starts are not a moral failing — they are a distributed systems tradeoff. Measure p99, budget concurrency, and only then spend money on provisioned capacity.