Understanding Lambda Cold Starts and Choosing Among Three Optimization Strategies

This article explains the mechanism behind Lambda cold starts from the Firecracker MicroVM lifecycle perspective, and compares optimization techniques across three axes: SnapStart, Provisioned Concurrency, and function design in terms of cost and constraints.

Why Do Cold Starts Happen?

To properly optimize Lambda cold starts, you first need to understand the mechanism behind them. Lambda runs functions on Firecracker MicroVMs. When a new request arrives and no reusable execution environment exists, AWS goes through a series of processes: starting the MicroVM, initializing the runtime, downloading and extracting the function code, and executing global scope code outside the handler. This entire initialization process is the cold start. The key point is that most of the cold start is under AWS's control - MicroVM startup and runtime initialization - and the only parts developers can directly control are package size and global scope initialization. For lightweight runtimes like Python or Node.js, AWS-side initialization completes in about 100-200ms, while Java and .NET require 500ms to several seconds just for runtime startup. This difference becomes a critical factor in runtime selection.

Cold Start Characteristics by Runtime

Cold start characteristics by runtime should be considered in the early stages of architecture design. The millisecond figures below are this article's own rough guides rather than values published by AWS, and they move up or down with memory settings, the volume of dependencies, and what the initialization code does. Node.js and Python are the lightest, with cold starts staying around 200-400ms even at 128MB memory settings. Go runs as a compiled binary with virtually zero runtime initialization overhead, making its cold starts the fastest at 100-200ms. Java, on the other hand, takes time for JVM startup and JIT compilation initialization, and cold starts of 3-10 seconds are not uncommon when using DI frameworks like Spring Boot. .NET also requires 500ms to 1 second for CLR startup. However, Java and .NET offer higher throughput during warm starts, making them advantageous for long-running batch processing and compute-intensive workloads. In other words, the optimal runtime depends on whether you prioritize cold start frequency or warm start performance. Node.js or Python for API backends where latency matters, and Java for batch processing is a rational approach.

Table: rough cold start figures by runtime (they shift with memory settings and implementation)
RuntimeTypical cold startCharacter and best fit
Go100-200 msRuns as a compiled binary, so runtime initialization overhead is close to zero
Node.js / Python200-400 ms, even at 128 MB of memoryThe lightest options; a good fit for latency-sensitive API backends
.NET500 ms to 1 second just to start the CLRWarm-start throughput is high
JavaJVM startup plus JIT initialization; 3-10 seconds is not unusual with a DI framework such as Spring BootSnapStart shortens the startup substantially, though how much depends on the application. Favourable for long-running batch and compute-heavy work

SnapStart - A Fundamental Solution to Java Cold Starts

SnapStart, announced at re:Invent 2022, is AWS's answer to the Java runtime cold start problem. SnapStart takes a snapshot of the initialized execution environment when a version is published, encrypts it, and caches it. On a cold start, the execution environment is restored from that snapshot instead of being initialized from scratch. This skips JVM startup and Spring Boot DI container initialization, so the startup time shrinks substantially. How much it shrinks depends on the size of the application and the weight of its initialization work, and there are cases where a startup of several seconds only comes down to a little over a second. To enable SnapStart, simply set the function's SnapStart ApplyOn to PublishedVersions. However, SnapStart has several constraints. When restoring from a snapshot, random values need to be regenerated and network connections re-established, so if your initialization code performs operations that depend on uniqueness (UUID generation, cryptographic key initialization, etc.), you need to re-initialize them in an afterRestore hook. Also, it cannot be used together with Provisioned Concurrency. Note that SnapStart is available only for managed runtimes, Java 11 and later among them, and cannot be used with container image-based functions. Which runtimes are supported, and whether the snapshot cache and the restore are billed for them, differ by language, so check the current support and pricing pages in the official documentation.

Provisioned Concurrency - Certainty at a Cost

Provisioned Concurrency is a feature that keeps a specified number of execution environments pre-warmed. Within the concurrency you configure, cold starts do not occur; invocations beyond that number are handled as ordinary ones, where a cold start can still happen. Since charges apply even when idle, cost planning is critical. Provisioned Concurrency pricing is calculated as provisioned concurrent executions multiplied by time. Charges accrue for the whole period the environments stay provisioned, so keeping a large concurrency provisioned around the clock reaches a scale that can consume a monthly budget on its own. Work the amount out from the GB-second rate for provisioned concurrency on the official pricing page for the region you use, and start from a design that reserves capacity only for the hours you need it. On top of that, execution time in the provisioned environments and per-request charges apply as well. To improve cost efficiency, combine it with Application Auto Scaling to dynamically adjust provisioning based on traffic patterns. For example, you can set schedule-based scaling with 100 during weekday business hours, 10 at night, and 5 on weekends. If the CloudWatch metric ProvisionedConcurrencySpilloverInvocations is non-zero, it signals that provisioning is insufficient. Conversely, if ProvisionedConcurrencyUtilization is consistently low, you can reduce provisioning to cut costs.

Function Design Optimization - What Developers Can Do Right Now

Even without SnapStart or Provisioned Concurrency, reviewing your function design alone can significantly reduce cold starts. The most impactful change is reducing package size. Lambda downloads and extracts the deployment package from S3 during cold starts, so larger packages mean longer initialization. For Node.js, bundle with esbuild or webpack and use tree-shaking to remove unused code. AWS SDK v3 has a modular design, so importing only the clients you need like @aws-sdk/client-s3 keeps the package smaller than bundling the entire SDK and cuts the time spent downloading and extracting it. For Python, separate common libraries into Lambda Layers to keep the function package lightweight. Memory settings are also an important optimization point. Lambda allocates CPU power proportional to memory, so increasing memory also speeds up cold start initialization. Simply going from 128MB to 512MB can halve initialization time in some cases. The AWS Lambda Power Tuning tool can automatically find the optimal memory setting that balances cost and performance.

Choosing Among the Three Optimization Approaches

The three cold start optimization approaches should be chosen based on your use case. For use cases like API Gateway backends where P99 latency directly impacts SLAs, Provisioned Concurrency is the most reliable option. Costs increase, but within the concurrency you configure cold starts do not occur; anything beyond the configured number is treated as an ordinary invocation, where a cold start can still happen. If you are using Java or .NET and cold starts exceed 1 second, first consider SnapStart. On managed Java runtimes it shortens the startup substantially with no additional cost, while support and billing differ for other runtimes, so check the official documentation before you count on it. If cold starts are under 500ms and within acceptable range, function design optimization alone is sufficient. Combining package size reduction, memory tuning, and connection pool initialization in global scope can achieve 200-300ms cold starts at no additional cost. For asynchronous processing (SQS triggers, EventBridge rules, etc.), cold starts of a few hundred milliseconds don't affect end users, so optimization priority can be lowered.

Table: the three optimization approaches side by side - cost, limits and fit
AspectSnapStartProvisioned ConcurrencyFunction design
EffectShortens Java startup substantially; how much depends on the applicationNo cold starts within the configured concurrency (invocations beyond it cold start as usual)Can reach 200-300 ms at no extra cost
Extra costNone on managed Java runtimes; differs for the other supported runtimesCharged continuously as provisioned concurrency multiplied by time; work the amount out from the GB-second rate for your region on the official pricing pageNone
Applies toManaged runtimes, Java 11 and later among them; check the official support tableEvery runtimeEvery runtime
Main limitsCannot be combined with Provisioned Concurrency, is unavailable for container image functions, and anything depending on uniqueness must be re-initialized in an afterRestore hookYou pay while idle, and you need Application Auto Scaling to follow the traffic patternYou cannot touch the MicroVM boot and runtime initialization that AWS owns
Best fitJava functions whose cold start exceeds one secondAPI backends where P99 latency feeds straight into an SLAWhen 500 ms is acceptable, and for asynchronous work triggered by SQS or EventBridge

References (Official AWS Resources)

The primary sources for this page are the official AWS website and documentation. Check the official pages below for the latest specifications and pricing.

If this page and the official documentation disagree, treat the official documentation as authoritative.