1. Cold_Start_Playbook
The Cold Start Playbook: Benchmarks, Trade-offs, and Fixes That Actually Work
Cold starts are the single most cited objection to serverless — but the gap between conventional wisdom and measured reality is wide enough to drive a fleet of EC2 instances through it. This chapter serves as your decision framework for when each optimization strategy pays off and where the hidden costs live.
Key Takeaways
- Most cold starts are 200–800ms, not the multi-second horror stories that dominate discussion — but the tail at p99 can exceed 5s for heavy runtimes
- Provisioned Concurrency eliminates cold starts but costs 2–3x the per-GB-hour rate; it only makes economic sense for steady baseline traffic
- Runtime choice is the single highest-leverage optimization: Go and Python start in under 500ms cold; Java and .NET routinely take 2–6s cold
- Code-level patterns (lazy loading, minimized deployment packages, SDK v3 tree-shaking) can shave 30–60% off your cold start — often with zero infrastructure cost
- SnapStart for Java is the closest thing to a free lunch: reduces Java cold starts by up to 90% with minimal code changes
---
You are 15 minutes into a production incident. Your team's API — a Node.js Lambda behind API Gateway — is serving p95 latencies of 4.2 seconds. Dashboards show a sustained spike in InitDuration across your function executions. The on-call engineer pings the Slack channel: "Are we seeing cold starts again?" The last deployment was two hours ago. Traffic was flat. Then, without warning, every concurrent invocation required a fresh environment spin-up.
You discover the root cause: a dependency update bloated the deployment package from 3 MB to 28 MB. Worse, an initialization-time database connection pool that previously warmed in 150ms now takes 1.8 seconds. Every concurrent burst exceeding the 10 reserved concurrency units triggers a new cold start. The "fix" — bumping provisioned concurrency — would cost $380/month for a function that averages 15 requests per second.
This is the cold start trilemma. You cannot simultaneously optimize for latency, cost, and simplicity. Pick two, and the third punishes you at the wrong moment.
Anatomy of a Cold Start
When a Lambda function is invoked and no pre-warmed execution environment is available, the service must allocate a sandbox, download the code, bootstrap the runtime, and run your initialization code — all before your handler receives the event. The timeline breaks down as follows:
timeline
title Lambda Cold Start Lifecycle (Typical Timeline)
t0 : Invocation Received
: No warm environment → Init phase begins
t1 : Download Code
: Package size depends (3-50MB typical)
t2 : Runtime Init
: Node.js: 50-100ms
: Python: 80-150ms
: Java: 500-2000ms
: .NET: 800-3000ms
: Go: 30-80ms
t3 : Handler Init
: DB connections, config loading, SDK clients
t4 : Handler Exec
: Actual business logic runs
The critical insight: cold starts are not a single number. The wall-clock delay is the sum of four independently optimizable phases:
| Phase | What Happens | Typical Range | Optimization Leverage | |-------|-------------|---------------|---------------------| | Download | Fetch code from S3 | 100–800ms per MB | Reduce package size, use layers | | Runtime bootstrap | Start language VM | 30ms (Go) – 3s (.NET) | Change runtime or use SnapStart | | Init code | Global scope + connections | 50ms – 2s+ | Lazy load, async init | | Handler | Business logic | Varies | Only optimizable per-function |
I started this analysis assuming that "provisioned concurrency is always the right fix for latency-sensitive workloads." What I found surprised me: provisioned concurrency solves the wrong problem when the dominant cost is in runtime bootstrap or deployment package size. More on that in a moment.
Provisioned Concurrency: The Two-Edged Shield
AWS Provisioned Concurrency keeps N execution environments initialized and ready — zero InitDuration, consistent sub-10ms response times from the infrastructure layer. It is the nuclear option for cold starts, and like nuclear options, it has fallout.
When it works: Your function has a predictable baseline traffic of at least 10–20 concurrent invocations per second, and p99 latency requirements under 200ms. The cost premium (~2x the on-demand per-GB-hour rate) amortizes across steady traffic. For a 512 MB function handling 1,000 requests/minute, provisioned concurrency of 10 adds roughly $35–50/month — a reasonable insurance premium for latency-sensitive endpoints.
When it fails: Variable traffic patterns force a choice between over-provisioning (waste) and under-provisioning (still getting cold starts on bursts). The worst-case scenario is a function with low average utilization but strict latency SLAs — you pay provisioned pricing for idle capacity.
graph LR
A["Traffic Pattern?"] --> B{"Predictable<br/>baseline?"}
B -->|"Yes"| C["Provisioned<br/>Concurrency"]
B -->|"No"| D{"Latency SLA?"}
D -->|"Strict<br/>(<200ms)"| E["Provisioned Concurrency<br/>(over-provision by 20%)"]
D -->|"Relaxed<br/>(<1s)"| F["On-Demand +<br/>Code Optimizations"]
C --> G["Cost: ~2x on-demand<br/>Latency: consistent <10ms"]
F --> H["Cost: on-demand<br/>Latency: 200ms-5s tail"]
E --> I["Cost: ~2.4x on-demand<br/>(20% over-provision)"]
style A fill:#f0f0f0,stroke:#333
style G fill:#e8f5e9
style H fill:#fff3e0
style I fill:#fff3e0
A 2024 analysis of 500 AWS Lambda functions across 15 production accounts found that only 23% of functions with provisioned concurrency had utilization rates above 60% (source: Lumigo State of Serverless 2024). The remaining 77% were effectively paying for insurance they didn't fully use — a valid business decision if the SLA requires it, but often a sign that teams defaulted to provisioned concurrency without investigating runtime or code optimizations first.
Imagine you're a platform engineer evaluating three candidate services for a new microservice. Your latency budget is 300ms p99. One uses Python (cold start ~300ms), one uses Java 21 (cold start ~1.8s without SnapStart), and one uses Go (cold start ~120ms). Provisioned concurrency on the Java service would cost $62/month. Switching to Go costs developer time — but the cold start is 15x faster without any provisioned concurrency at all. The trade-off is be
11m / Article + audio + video