AWS Lambda Cold Starts: Mitigation and Trade-offs
How AWS Lambda cold starts work, what they cost since the 2025 INIT billing change, and how to cut them with SnapStart, provisioned concurrency and design.

AWS Lambda cold starts are the latency penalty you pay when Lambda has to build a fresh execution environment before it can run your code. For most functions this is invisible. For a user-facing API on a tight timeout budget — or a two-gigabyte container image full of headless Chromium — it is the number people obsess over. This guide explains where cold start latency comes from, what it costs now that AWS bills the INIT phase, and how to decide between SnapStart, provisioned concurrency, faster runtimes, and plain architecture. The goal is not to eliminate cold starts. It is to control which requests ever see one.
Quick Answer
An AWS Lambda cold start is the one-time INIT phase Lambda runs when it creates a new execution environment: a Firecracker microVM boots, your language runtime bootstraps, and your initialization code runs. It typically adds anywhere from ~100 ms to several seconds depending mostly on runtime and dependency size, and AWS reports it affects under 1% of invocations in steady traffic. The most effective mitigations are SnapStart (free for Java, available for Python 3.12+ and .NET 8+), provisioned concurrency for hard latency SLAs, choosing a fast runtime like Go or Rust, and — most underrated — routing latency-tolerant work down asynchronous paths so cold starts never reach a waiting user.
What Is a Cold Start?
A Lambda execution environment goes through three phases: INIT, INVOKE, and SHUTDOWN. The INIT phase only runs on a cold start — when Lambda spins up a new environment because it is the first request, it is scaling up to add concurrency, or you just deployed. Once an environment exists, Lambda freezes it between requests and reuses it; those reused invocations are warm starts and skip INIT entirely.
During INIT, Lambda downloads your code (or pulls your image from ECR), provisions the microVM, bootstraps the language runtime, and runs your init code — the module-level work above your handler, such as creating SDK clients and opening connections. Only then does the handler run. The mental model that matters: a cold start is not one cost, it is a stack of costs, and the one your dashboard shows you (Init Duration) is often the smallest.

The Problem: Cold Starts Are Now a Latency and a Cost Line Item
Two things make cold starts worth engineering time in 2026.
The first is latency. On a synchronous, user-facing path, a multi-second cold start is a request that hangs or times out — a real person waiting on a freshly scaled function.
The second is new. Since August 1, 2025, AWS bills the INIT phase for all functions. Previously, on-demand functions using managed runtimes with ZIP packaging got INIT time for free; now it is billed at the same per-GB-second rate as your handler. AWS says most users see minimal impact because INIT happens on a small fraction of invocations, but heavy-init Java and .NET functions on low-traffic workloads can see a real increase. Cold starts used to be purely a UX problem; now they show up on the bill too.
The Solution: Understand the Lifecycle, Then Target the Right Phase
You cannot fix what you cannot see, and the headline metric hides most of the cost. Here is real data from a production PDF-rendering function — a container image carrying headless Chromium, LibreOffice, a Java runtime, and four font families, near two gigabytes of dependencies. Three consecutive CloudWatch traces of one payload against one deploy: one cold, two warm.
| Phase | Cold | Warm |
|---|---|---|
| Runtime init | 1002 ms | 0 ms |
| Chromium unpack to `/tmp` | 4949 ms | 0 ms |
| Page setup + CSS inlining | 3623 ms | 1066 ms |
| **Cold-start overhead** | **~5951 ms** | **0 ms** |
| Full request (warm) | — | 2523 ms |
Two lines change how you think about mitigation.
`Init Duration` is a minority of the real cost. AWS reported 1002 ms of init — about 17% of the overhead. The largest single item, nearly five seconds decompressing a Brotli-packed Chromium binary into /tmp, happens inside the handler, on billed duration, invisible to every cold-start dashboard. Anything a library lazily initializes on first use is cold-start cost you pay and never measure.
The tax outlives the init phase. Page setup took 3623 ms cold and 1066 ms warm — identical code and input, no network either way. That 3.4x gap is JIT and first-run warm-up: cold starts do not stop charging when INIT ends. The lesson — measure the whole first request, not the number on your dashboard.
Where the latency lives (Firecracker)
Every Lambda runs inside a Firecracker microVM — a lightweight, Rust-based virtual machine that boots in around 125 milliseconds. That number is essentially fixed. The variable, controllable part of a cold start is everything after it: runtime bootstrap, dependency loading, and your init code. Firecracker's snapshot capability is also exactly what SnapStart exploits — which is why SnapStart is so effective for slow-booting runtimes.
Prerequisites
To follow the mitigations below you will want an AWS account with Lambda access, familiarity with your deployment tool (AWS SAM, CDK, Serverless Framework, or Terraform), a function you can measure (CloudWatch Logs access at minimum), and a basic understanding of your runtime's startup behavior — JVM/CLR bootstrap versus interpreted versus native.
What Causes and Worsens Cold Starts
Before reaching for mitigations, know what you are fighting, in rough order of impact. Runtime choice dominates — compiled runtimes (Rust, Go) start fastest, interpreted ones (Python, Node.js) are moderate, and VM/JIT runtimes (Java JVM, .NET CoreCLR) are slowest to bootstrap. Package and image size add download and module-load time. Memory is really the CPU dial — Lambda gives one full vCPU at 1,769 MB, so undersized memory slows init, especially for JVM and .NET. VPC attachment was historically catastrophic but is now largely solved (below). And container images can be slower on the first invocation of a fresh deploy, before AWS's block-level image cache warms.
Typical cold start latency by runtime
Numbers vary widely with memory, package size, and methodology — treat these as representative ranges:
| Runtime | Typical cold start (minimal handler) |
|---|---|
| Rust (`provided.al2023`) | ~15–25 ms |
| Go (`provided.al2023`) | ~40–60 ms |
| Python 3.12/3.13 | ~90–400 ms |
| Node.js 20/22 | ~120–400 ms |
| Java 21 (JVM, no SnapStart) | ~400 ms to several seconds |
| .NET 8 (no Native AOT) | ~800 ms to ~3 s |
Java has by far the widest spread: a minimal handler can be ~400 ms, while a Spring Boot application can exceed three seconds. Runtime is the dominant variable; memory and package size are multipliers.
A note on VPC (mostly a solved problem now)
Before 2019, attaching a function to a VPC created an Elastic Network Interface per environment and could add ten seconds or more. AWS re-architected this with Hyperplane shared ENIs: the ENI is created once when the function or its VPC config changes, and per-invocation overhead dropped to sub-second or negligible. Older advice to "never put Lambda in a VPC" is out of date — though avoiding an unnecessary one still removes a variable.
Step-by-Step: Mitigation Strategies and Their Trade-offs
There is no single fix. Each strategy targets a different phase at a different cost. Work through them roughly in order.
Step 1: Optimize Code, Packages, and Memory (Always Worth It)
This is the positive-ROI baseline you do regardless of anything else.
- Trim the artifact. Tree-shake, bundle, and minify; on Node.js, use the modular AWS SDK v3 to import only the clients you need.
- Reuse clients across invocations — instantiate SDK clients and database connections once, at module scope.
- Move heavy work into the init phase deliberately. It runs at full CPU regardless of your memory setting, so work moved there runs on faster hardware for free.
- Right-size memory. Because memory is the CPU dial, bumping it toward 1,769 MB can reduce total cost by cutting duration. Use AWS Lambda Power Tuning to find the optimum.
- Consider arm64/Graviton — typically slightly faster to start and ~20% cheaper per GB-second.
Step 2: Enable SnapStart (If Your Runtime Supports It)
SnapStart runs your INIT once, snapshots the initialized microVM, and restores from it on future cold starts instead of re-running init. For slow-booting runtimes it is the highest-leverage fix available.
It supports Java 11+, Python 3.12+, and .NET 8+ only — not Node.js, Ruby, or container images. It is free for Java managed runtimes; for Python and .NET you pay a caching charge (three-hour-per-version minimum) plus a per-restore charge, so model the cost at low volume before assuming it is free.
SnapStart requires a published version and alias:
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: java21
Handler: com.example.Handler::handleRequest
MemorySize: 1024
AutoPublishAlias: live # SnapStart requires a published version + alias
SnapStart:
ApplyOn: PublishedVersionsThe critical caveat: the snapshot is shared across every restored environment, so anything unique generated during INIT gets frozen and duplicated — random seeds, UUIDs, cached credentials, open connections. The fix is to generate uniqueness inside the handler or in an afterRestore runtime hook, and to re-establish network connections after restore rather than trusting the ones baked into the snapshot.
Step 3: Use Provisioned Concurrency for Hard Latency SLAs
Provisioned concurrency pre-initializes a fixed number of environments and keeps them warm 24/7, so requests within that capacity skip INIT entirely; traffic above the count spills to on-demand and can still cold start.
It is the most predictable low-latency option and the only one that gets Node.js and Ruby out of cold starts. The trade-off is cost: you pay for the allocation every second it is enabled, traffic or not, and it does not draw from the free tier. Datadog, citing AWS, notes it is most cost-effective above roughly 60% sustained utilization — pair it with Application Auto Scaling to match known traffic curves rather than paying for idle capacity.
In SAM:
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs22.x
Handler: index.handler
AutoPublishAlias: live
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 5One honest limitation: provisioned concurrency only eliminates the init phase. In the PDF example that is 1002 ms of a ~5951 ms problem — it does nothing for the five-second Chromium unpack, which runs inside the handler. Useful, priced continuously, and not the blanket fix it is often described as.
Step 4: Choose a Faster Runtime for Greenfield Latency-Critical Work
If you are starting fresh and latency is critical, runtime choice beats every other lever. Go and Rust deliver sub-100 ms — often sub-30 ms for Rust — cold starts with small memory footprints. For JavaScript, AWS Labs' experimental LLRT runtime claims up to 10x faster startup, but it is explicitly not production-ready and not a drop-in Node replacement — one to watch, not adopt.
Step 5: Match Invocation Mode to Cold-Start Tolerance (The Big One)
This strategy removes the problem instead of shrinking it — an architecture decision, not a runtime setting.
The PDF function serves two paths with completely different tolerances. Merge runs off SQS and reports back over a webhook — asynchronous by design, so six seconds of cold start on a job whose result arrives by callback is invisible, and SQS adds retries, batching, and a dead-letter queue for free. Render is invoked synchronously with a caller on a timeout — the only place cold start is actually user-facing.
Splitting those paths does more than any runtime optimization: it removes cold start as a concern for an entire class of requests. Send everything latency-tolerant down an async path; save your effort for the synchronous path that remains. Bursts help too — fifty merges arriving together pay a handful of cold starts, then reuse warm containers, because cold start is a per-container cost, not a per-request one.

Common Problems and Errors
Undercounting cold starts. Suppressed inits (when a provisioned environment is not ready and INIT runs inline) do not emit a separate @initDuration, so Logs Insights queries filtering on @initDuration > 0 quietly undercount — and lazy in-handler init never appears in Init Duration at all.
SnapStart uniqueness bugs. Seeding a random generator, minting UUIDs, or caching secrets at init means every restored environment produces identical "unique" values. Generate uniqueness in the handler or an afterRestore hook.
Stale connections after SnapStart restore. A database connection captured in the snapshot is dead on arrival — re-establish it after restore.
Provisioned concurrency not matched to traffic. Over-provision and you pay for idle capacity; under-provision and you still cold start on spillover. Monitor ProvisionedConcurrencySpilloverInvocations and utilization, and use Application Auto Scaling.
Warmer pings as a production strategy. Pinging every five minutes is now an anti-pattern: it does not guarantee enough warm environments for a burst, clutters your handler with branching, and AWS already does free proactive initialization.
Best Practices
- Measure the full first request, not just `Init Duration` — the dashboard number is often a minority of the real cost.
- Bake setup into the image instead of paying it per container. In the PDF function, LibreOffice's slow first-run profile and font caches are generated at build time (
fc-cache, a throwaway conversion) so CI pays once and no user does. The image pointedly does not do this for Chromium, which unpacks at runtime — the 4949 ms line in the table. - Reuse `/tmp` across warm invocations, but treat it as real state crossing request boundaries: it takes the Chromium unpack to zero on the second request, and obliges a
browser.close()in afinallyblock or orphaned processes fill/tmpuntil the container dies. - Pin your base image by digest, not a floating tag — numbers measured against a moving base are not comparable across deploys, and native binaries can stop starting when the OS underneath shifts.
- Route latency-tolerant work to async paths before optimizing anything, and re-evaluate mitigations quarterly to drop any not earning their cost.
Performance and Security Considerations
Two things are easy to miss. Memory is CPU, so CPU-bound work (decompression, CSS inlining, PDF encoding) gets faster as you raise the memory setting — on a spiky workload, pay-per-millisecond beats reserving continuous headroom. And the SnapStart uniqueness caveat is a real security concern, not just correctness: duplicated tokens and predictable randomness across restored environments open genuine attack surface, so generate anything sensitive in the handler, never at init.
Alternatives and Comparison
| Strategy | Eliminates INIT? | Handler-phase cost? | Cost model | Best for |
|---|---|---|---|---|
| Code/package/memory tuning | Partial | Reduces it | Free (effort only) | Everything, always |
| SnapStart | Yes | No | Free (Java) / low (Py, .NET) | Slow JVM/CLR runtimes |
| Provisioned concurrency | Yes | No | Always-on | Node/Ruby, hard SLAs |
| Fast runtime (Go/Rust) | Shrinks it | Lower | Free at runtime | Greenfield latency-critical |
| Async architecture | Hides it | Unaffected | Free | Any latency-tolerant work |
When Should You Mitigate Cold Starts?
Worth engineering on synchronous, user-facing paths with a tight timeout budget, long synchronous Lambda chains where cold starts stack, or bursty zero-to-thousands scale-outs on latency-sensitive endpoints. Skip it for async or event-driven workloads (SQS, Kinesis, S3), batch and ETL where an extra second is invisible, and low-traffic internal tools. The most common mistake is spending real money — provisioned concurrency across dozens of functions — to fix a cold start that never reached a user. Measure before you spend.
FAQ
How long does an AWS Lambda cold start take?
Anywhere from ~15 ms for a minimal Rust function to several seconds for a large Java or container-based function. Runtime is the biggest factor, then package size and memory; most interpreted-runtime functions land in the 100–400 ms range.
Does provisioned concurrency eliminate cold starts completely?
It eliminates the INIT phase for the capacity you provision, but not work that runs inside your handler (like lazy dependency unpacking), and traffic above your provisioned count still spills to on-demand and can cold start. It is also billed continuously whether or not it serves traffic.
Does SnapStart work with Node.js or container images?
No. SnapStart supports Java 11+, Python 3.12+, and .NET 8+ only. Node.js, Ruby, OS-only runtimes, and container images are not supported. For those, provisioned concurrency or a faster runtime is the path.
Why is my cold start bigger than my Init Duration metric?
Because Init Duration only measures the INIT phase. Anything a library initializes lazily on first use — unpacking a binary, opening a connection, JIT warm-up — runs inside the handler on billed duration and never shows up in that metric. Measure the whole first request.
Are Lambda cold starts still a problem inside a VPC?
Not the way they used to be. Since AWS moved to Hyperplane shared ENIs in 2019, VPC-related overhead dropped from ten-plus seconds to sub-second or negligible. Advice to avoid VPCs purely for cold starts is out of date.
Conclusion
AWS Lambda cold starts are real and, for dependency-heavy functions, not fully optimizable — a two-gigabyte renderer will cold start in seconds no matter what you do. But that is the wrong thing to fixate on. The problem is not the cold start's existence; it is which requests are allowed to see one. Route latency-tolerant work down an async path, engineer the synchronous path with SnapStart, provisioned concurrency, or a faster runtime as the case demands, and measure the cold start you actually have. Now that AWS bills the INIT phase, getting this right is a cost decision as much as a latency one. Six seconds is a lot — but it is only paid by the requests you decided could afford it.
Need Help Building or Optimizing Serverless on AWS?
If you are running latency-sensitive or dependency-heavy workloads on AWS Lambda and want them fast, reliable, and cost-efficient, our team can help. We design, build, and optimize serverless architectures — from cold-start tuning and SnapStart rollouts to async pipelines and container-based Lambda functions. Get in touch to discuss your project.

Aarav Sharma
Lead Software Engineer
Aarav leads product engineering at Matlab Infotech, where he has shipped mobile and web platforms across healthcare, fintech, and SaaS. He writes about pragmatic engineering and shipping fast without cutting corners.