How to Reduce AWS Lambda Cold Starts: Init Duration to SnapStart (2026)
This guide is for engineers who own an AWS Lambda function with a latency problem, or a bill that grew after AWS started charging for initialisation. By the end you will have a cold start baseline from your own logs, a cost figure for it, and a ranked set of fixes applied in order of cost: runtime and architecture, package size, lazy loading, memory, SnapStart, then provisioned concurrency. Everything here was checked against the AWS Lambda developer guide and price list in October 2026, with AWS CLI v2 (I used 2.27.7), Node.js 24, Python 3.13 and Java 21/25 runtimes in mind. You need the AWS CLI configured for an account where you can update the function, and its log group in CloudWatch Logs.
Step 1: Measure cold starts with Init Duration
A cold start is the time Lambda spends creating a new execution environment: downloading code, starting extensions and the runtime, and running your code outside the handler. The execution environment docs put cold starts at "under 1% of invocations" typically, lasting from under 100 ms to over a second. Your numbers are the only ones that matter, and they are in the REPORT line Lambda writes for every invocation. A cold invocation has an extra Init Duration field:
REPORT RequestId: 527cb862-... Duration: 3016.78 ms Billed Duration: 3101 ms Memory Size: 128 MB Max Memory Used: 31 MB Init Duration: 84.00 ms
Run this CloudWatch Logs Insights query against the function's log group to get counts and percentiles per hour. It works on the default plain text log format, where Lambda exposes @initDuration as a discovered field.
# Last 24 hours of cold start stats for one function (plain text log format).
FN=my-function
QUERY_ID=$(aws logs start-query \
--log-group-name "/aws/lambda/${FN}" \
--start-time $(( $(date +%s) - 86400 )) \
--end-time $(date +%s) \
--query-string 'filter @type = "REPORT"
| stats count(*) as invocations,
count(@initDuration) as coldStarts,
pct(@initDuration, 50) as initP50,
pct(@initDuration, 99) as initP99,
max(@initDuration) as initMax
by bin(1h)' \
--query queryId --output text)
sleep 5
aws logs get-query-results --query-id "$QUERY_ID" --output table
If you switched the function to JSON log format, the same data lives in platform.report records, with the init time at record.metrics.initDurationMs per the telemetry schema:
filter type = "platform.report"
| stats count(*) as invocations,
count(record.metrics.initDurationMs) as coldStarts,
pct(record.metrics.initDurationMs, 99) as initP99
Write down three numbers: cold start percentage, init p50 and init p99. Also note how much of your tail latency the cold starts explain. If p99 is driven by a slow database query, none of the steps below will help. SnapStart functions do not log Init Duration in REPORT; they log Restore Duration instead, which Step 7 covers.
Step 2: Work out what cold starts cost after the INIT billing change
Since 1 August 2025, AWS bills the INIT phase for on-demand invocations of ZIP-packaged functions on managed runtimes. Container images, custom runtimes and provisioned concurrency already paid for it. In practice, Billed Duration on a cold invocation now includes Init Duration. Price it with the us-east-1 price list: $0.0000166667 per GB-second on x86 and $0.0000133334 on arm64 (first tier).
# Monthly INIT cost = cold starts x init seconds x memory GB x price per GB-s
# Example: 10M invocations, 2% cold, 800 ms init, 1 GB, arm64
python3 -c "print(round(10_000_000*0.02 * 0.8 * 1.0 * 0.0000133334, 2))"
# 2.13
For most functions that is pocket change, and the real cost is latency. It stops being pocket change for multi-second Java or data science inits at high scale-out rates, and when it does, it is a direct argument for Steps 3 to 7.
Step 3: Choose the runtime and switch to arm64
Runtime choice sets the floor. The runtimes page notes that interpreted languages like Python and Node.js start fastest for simple functions, while Java is "often slower to initialize". Two housekeeping items first: Node.js 20 was deprecated on 30 April 2026 and Python 3.10 is deprecated on 31 October 2026, so if you are on either, move to nodejs24.x or python3.13/python3.14 while you are here. Amazon Linux 2 reached end of life on 30 June 2026.
Then move to arm64. AWS's launch claim for Graviton was up to 34% better price performance, workload dependent, and the price list confirms the 20% lower GB-second rate. It does not reliably shorten init by itself, but it makes every billed init millisecond cheaper.
# Check current runtime, architecture and memory
aws lambda get-function-configuration --function-name my-function \
--query '{Runtime:Runtime,Arch:Architectures,Memory:MemorySize,Pkg:PackageType}'
# Switch architecture: architecture is set with the code, not the config.
aws lambda update-function-code --function-name my-function \
--zip-file fileb://fn.zip --architectures arm64
Rebuild any native dependencies (Python wheels, Node addons, Rust or Go binaries) for aarch64 first, or the function will fail on import.
Step 4: Shrink the deployment package
Package size and the amount of code your runtime parses at startup are the biggest levers you control. The docs say it directly: initialisation latency depends on "the size of the function package, in terms of imported libraries and dependencies, and Lambda layers". For Node.js, bundle and minify with esbuild so the runtime loads one file instead of walking node_modules, and import modular AWS SDK v3 clients instead of whole SDKs.
# Node.js 24 handler using DynamoDB via AWS SDK v3, bundled with esbuild.
npm install @aws-sdk/client-dynamodb@3.1153.0 @aws-sdk/lib-dynamodb@3.1153.0
npm install --save-dev esbuild@0.28.2
npx esbuild src/index.mjs --bundle --minify --platform=node \
--target=node24 --format=esm --outfile=dist/index.mjs
cd dist && zip -q -9 ../fn.zip index.mjs && cd ..
When I ran exactly this on a small DynamoDB GetCommand handler, the runtime dependencies took about 17 MB in node_modules (the @aws-sdk and @smithy trees), and the output was:
dist/index.mjs 523.4kb
⚡ Done in 52ms
# fn.zip: 156,517 bytes
For Python, the equivalent is ruthless dependency pruning: no test libraries, no boto3 vendored unless you pin a version the runtime lacks, and avoid pulling pandas or numpy into a function that does not need them. Check what you ship:
# Largest packages in a Python build directory
du -sh package/* | sort -rh | head -15
For container images, keep the image on an AWS base image and small; image functions already paid for INIT before August 2025, so nothing changed for them except that ZIP functions lost their price advantage on init.
Step 5: Lazy-load what only some requests need
Code at module level runs on every cold start, and it is billed. The static initialisation guidance is to keep reusable clients and connections outside the handler, but to load rarely used dependencies lazily. Both halves matter.
# handler.py (python3.13): keep hot-path clients global, load heavy paths lazily
import json
import os
import boto3
# Created once per environment and reused on warm invocations.
s3 = boto3.client("s3")
_pdf = None # heavy optional dependency, loaded on first use only
def _get_pdf():
global _pdf
if _pdf is None:
import pypdf # imported only by the few requests that need it
_pdf = pypdf
return _pdf
def lambda_handler(event, context):
if event.get("action") == "extract_pdf":
reader = _get_pdf().PdfReader("/tmp/in.pdf")
return {"pages": len(reader.pages)}
obj = s3.get_object(Bucket=os.environ["BUCKET"], Key=event["key"])
return json.loads(obj["Body"].read())
Do not fetch secrets or open database connections at import time if a timeout there can fail the whole init. On-demand init is limited to 10 seconds; past that, Lambda retries init on the first invocation with your function timeout, and you see INIT_REPORT ... Status: timeout in the logs.
Step 6: Tune memory size for init, not only for the handler
CPU scales with memory: per the memory docs, 1,769 MB equals one vCPU, and the range is 128 MB to 10,240 MB. A function at 128 MB gets a fraction of a core to parse code and start the runtime, so raising memory often cuts Init Duration and Duration enough to cost less overall. Measure rather than guess, with the open source AWS Lambda Power Tuning state machine, which the AWS docs recommend, or with a quick manual sweep:
for MB in 512 1024 1769 3008; do
aws lambda update-function-configuration --function-name my-function \
--memory-size $MB --query MemorySize --output text
aws lambda wait function-updated --function-name my-function
# A configuration change forces fresh environments, so the next call is cold.
aws lambda invoke --function-name my-function --payload '{}' \
--cli-binary-format raw-in-base64-out --log-type Tail /dev/null \
--query LogResult --output text | base64 --decode | grep REPORT
done
One cold sample per size is noisy. Repeat each size a few times, then confirm with the Step 1 query over a day of real traffic.
Step 7: Enable SnapStart for Java, Python or .NET
SnapStart initialises your function when you publish a version, snapshots the Firecracker microVM, and restores new environments from that snapshot. The SnapStart docs list Java 11 and later, Python 3.12 and later, and .NET 8 and later, on ZIP and container images. Node.js, Ruby and OS-only runtimes are not supported, nor are provisioned concurrency, EFS or ephemeral storage above 512 MB. It only applies to published versions and aliases, never $LATEST.
aws lambda update-function-configuration --function-name my-function \
--snap-start ApplyOn=PublishedVersions
aws lambda wait function-updated --function-name my-function
VERSION=$(aws lambda publish-version --function-name my-function \
--query Version --output text)
aws lambda get-function-configuration --function-name "my-function:${VERSION}" \
--query '{SnapStart:SnapStart,State:State}'
# Expect: "OptimizationStatus": "On" and "State": "Active"
# Point the alias your trigger uses at the new version.
aws lambda update-alias --function-name my-function --name live \
--function-version "$VERSION"
Before you enable it, read the uniqueness rules: anything random generated during init (IDs, seeds, secrets) is frozen into the snapshot and shared by every restored environment. Generate those in the handler or in an after-restore hook. In Python the hooks come from the snapshot_restore_py library, included in the managed runtime:
from snapshot_restore_py import register_after_restore, register_before_snapshot
import boto3
s3 = boto3.client("s3") # AWS SDK connections resume automatically after restore
@register_before_snapshot
def warm():
# Runs once at publish time: preload models, templates, framework imports.
import pydantic # noqa: F401
@register_after_restore
def refresh():
# Runs on every restore, must finish within the 10 second restore limit.
pass
Measure cold starts on SnapStart functions as Restore Duration plus Duration; the monitoring page gives a full percentile query. Pricing: no extra charge for Java; Python and .NET pay a cache fee of $0.0000015046 per GB-second for each active version (minimum 3 hours) and $0.0001397998 per GB restored, per the us-east-1 price list. Delete old versions, because each cached snapshot keeps billing.
Step 8: Use provisioned concurrency for hard p99 targets
If the latency target is strict, or the runtime is Node.js where SnapStart does not exist, provisioned concurrency keeps initialised environments ready. The docs describe "double-digit millisecond" response times. It must target an alias or version, and you pay $0.0000041667 per GB-second on x86 ($0.0000033334 on arm64) for every configured environment, used or not, plus a lower duration rate.
aws lambda put-provisioned-concurrency-config --function-name my-function \
--qualifier live --provisioned-concurrent-executions 10
aws application-autoscaling register-scalable-target --service-namespace lambda \
--resource-id function:my-function:live --min-capacity 2 --max-capacity 50 \
--scalable-dimension lambda:function:ProvisionedConcurrency
aws application-autoscaling put-scaling-policy --service-namespace lambda \
--scalable-dimension lambda:function:ProvisionedConcurrency \
--resource-id function:my-function:live --policy-name pc-70 \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration \
'{"TargetValue":0.7,"PredefinedMetricSpecification":{"PredefinedMetricType":"LambdaProvisionedConcurrencyUtilization"}}'
Size it with the formula from the docs, concurrency equals requests per second times average duration in seconds, plus a 10% buffer. Watch ProvisionedConcurrencySpilloverInvocations: anything above zero means requests overflowed onto cold on-demand environments.
If you need provisioned concurrency around the clock at high utilisation, you are paying for capacity anyway. Price the same function on Lambda Managed Instances, which scale on CPU "without cold starts" at EC2 prices plus a 15% fee. I compare those options in Serverless in 2026.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Cold starts continue after enabling provisioned concurrency | Trigger invokes $LATEST or an unqualified ARN |
Point the event source, API integration or URL at the alias with provisioned concurrency |
ProvisionedConcurrencySpilloverInvocations above zero |
Provisioned concurrency too low for bursts | Raise min capacity; bursts shorter than 3 minutes may need the Maximum statistic or scheduled scaling |
INIT_REPORT Init Duration: ... Status: timeout |
On-demand init exceeded 10 seconds | Move slow work out of module scope (Step 5), or use SnapStart or provisioned concurrency, which allow longer init |
SnapStartNotReadyException on invoke |
Version still Pending, or a Java snapshot expired after 14 days without invocations | Wait for State Active, then invoke again |
SnapStartTimeoutException |
Runtime load plus after-restore hooks exceeded 10 seconds | Move work from after-restore hooks into before-snapshot hooks or the handler |
| Duplicate IDs or identical random values across environments | Unique values generated during init and frozen in the snapshot | Generate them in the handler or an after-restore hook |
Billed Duration jumped since August 2025 with no code change |
INIT phase now billed for on-demand ZIP functions | Expected; reduce init with Steps 3 to 6 |
Runtime.ImportModuleError or exec format error after moving to arm64 |
Native dependency built for x86_64 | Rebuild wheels, addons or binaries for aarch64 |
No Init Duration in REPORT on a SnapStart function |
SnapStart logs Restore Duration; init is in INIT_REPORT at publish time |
Use the restore-aware query from the SnapStart monitoring page |
| Provisioned concurrency never scales down overnight | No traffic means no utilisation metric, so alarms sit in INSUFFICIENT_DATA |
Add scheduled scaling for quiet hours |
FAQ
Does Lambda charge for cold starts?
Yes. Since 1 August 2025 the INIT phase is billed for on-demand ZIP functions on managed runtimes, as it already was for container images, custom runtimes and provisioned concurrency. You can see it in the REPORT line, where Billed Duration includes Init Duration on cold invocations.
Does SnapStart work with Node.js?
No. SnapStart supports Java 11 and later, Python 3.12 and later, and .NET 8 and later. For Node.js, the options are smaller bundles, lazy loading, more memory and provisioned concurrency, or the new Lambda Web Functions, which serve many requests per environment.
Is SnapStart or provisioned concurrency better?
SnapStart is cheaper and usually good enough: it removes most of the init work with no idle capacity charge for Java. Provisioned concurrency gives the most predictable latency and works on every runtime, but you pay for every configured environment all the time. AWS recommends provisioned concurrency when SnapStart cannot meet a strict cold start requirement.
Does VPC attachment still cause slow cold starts?
Not the way it used to. Since September 2019, Lambda creates the network interface when you create or update the function rather than at invocation. Slow VPC cold starts today usually come from DNS or connection setup in your init code.
When a team asks me about cold starts, the first thing I check is whether they matter at all: the Step 1 query, and whether cold starts actually explain p99 or a slow dependency does. Most of the time the fix is in Steps 3 to 6, and it costs nothing but an afternoon. I only reach for provisioned concurrency for user-facing paths with a written latency target, and once it runs all day I price Managed Instances next to it. The same logic applies to my own Firecracker work on PandaStack (my company, a microVM cloud for agents), where snapshot restore beats keeping things warm, which I wrote up in What Scale to Zero Cost Me to Build. Official references: execution environment lifecycle, SnapStart, provisioned concurrency and Lambda pricing.
Related: I Measured Every Stage of My 179ms Firecracker Boot Path, Sandbox Creation Time Benchmarks Measure Different Things and Serverless in 2026: Lambda vs Cloud Run vs Workers, and When It Costs More.
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
How to Troubleshoot Linux Performance in 60 Seconds (2026 Checklist)
A step-by-step Linux performance troubleshooting guide for 2026: Brendan Gregg's 60-second checklist (uptime, dmesg, vmstat, mpstat, pidstat, iostat, free, sar, top), how to read each output, PSI, the USE method, then bpftrace 0.27 and BCC one-liners.
13 minOct 20, 2026How to Reduce Docker Image Size: Multi-Stage Builds Guide (2026)
Shrink Docker images with multi-stage builds, distroless or slim bases, .dockerignore, layer ordering and BuildKit cache mounts. Measured Go, Node, Python and Java examples: 343 MB to 3.4 MB, 423 MB to 56 MB, plus a troubleshooting table.
12 minOct 20, 2026Serverless in 2026: Lambda vs Cloud Run vs Workers, and When It Costs More
Serverless in 2026 explained: AWS Lambda Managed Instances, durable functions, SnapStart and the new Web Functions, Cloud Run GPUs and worker pools, Azure Container Apps, Cloudflare Containers and Vercel Fluid compute, with a list-price comparison and the workloads where serverless costs more.
14 min