Cold starts are the most over-discussed and least-measured problem in serverless. In 2026 the tooling to eliminate them is mature, but each option has a cost and a set of conditions where it does nothing. This post is the checklist we run on every performance-tuning engagement, in the order we run it, with Serverless Framework V4 configuration for each step.
First: measure, do not guess
A cold start is the time Lambda spends creating an execution environment and running your initialization code before the handler is invoked. It shows up in CloudWatch as Init Duration in the REPORT log line, and as a separate segment in X-Ray traces. Before changing anything, answer three questions with data:
- What fraction of invocations are cold? Query CloudWatch Logs Insights:
filter @type = "REPORT"
| stats count(*) as invocations,
sum(strcontains(@message, "Init Duration")) as coldStarts,
avg(@initDuration) as avgInit, pct(@initDuration, 95) as p95Init
- How long is the init?
p95Initabove 500 ms is worth fixing; 100 ms usually is not. - Does anyone notice? A nightly batch job does not care. A login endpoint does.
If cold starts are under 2% of invocations and under 300 ms, stop here and spend the effort elsewhere.
Step 1: Right-size memory (it is also CPU)
Lambda allocates CPU proportionally to memory. A 128 MB function gets a sliver of a vCPU; 1,769 MB gets a full one. Initialization is CPU-bound (parsing JavaScript, loading modules, establishing TLS connections), so a function starved of memory has slow cold starts and slow warm invocations.
Run AWS Lambda Power Tuning against each function. It deploys a Step Functions state machine that invokes your function at several memory sizes and plots cost versus duration. The common finding: going from 256 MB to 1024 MB makes the function faster and cheaper, because it finishes in a fraction of the time.
functions:
api:
handler: src/api.handler
memorySize: 1024 # from the Power Tuning result, not a guess
architecture: arm64 # Graviton: lower price per ms, often faster init
Step 2: Shrink and trim the bundle
Init time scales with the amount of code Node has to load. Three changes that consistently help:
Bundle and tree-shake. V4's built-in esbuild does this; make sure it is on and that you are not shipping node_modules wholesale:
build:
esbuild:
bundle: true
minify: true
target: node22
exclude:
- '@aws-sdk/*' # provided by the runtime
Import only the SDK clients you use. @aws-sdk/client-dynamodb instead of the whole v2 aws-sdk. If any function still imports aws-sdk v2, that alone can add hundreds of milliseconds.
Do expensive work lazily. Anything at module scope runs during init. Create SDK clients at module scope (cheap, and reused across invocations), but defer things like loading a large config file or warming a cache until the first request that needs it.
Check your result with serverless package and look at the zip sizes under .serverless/. Under 1 MB per function is a reasonable target for an API handler.
Step 3: Provisioned Concurrency for Node.js
When measurements say the cold start still matters, Provisioned Concurrency keeps a configured number of execution environments initialized and ready. It eliminates cold starts for requests served by those environments; traffic above the provisioned count still scales on demand with normal cold starts.
functions:
api:
handler: src/api.handler
provisionedConcurrency: 5
Two things to understand before you turn it on:
- You pay for it continuously, whether or not requests arrive. Provisioned environments are billed per GB-second of readiness plus a lower per-invocation rate. For a function with a steady daytime load it is often cheap; for a spiky one it can double the bill.
- It attaches to a version or alias, not
$LATEST. The Serverless Framework handles the versioning for you, but it means every deploy rotates the provisioned environments and briefly re-initializes them.
Scale it with Application Auto Scaling on a schedule (business hours up, nights down) rather than leaving a flat number on all day. The Provisioned Concurrency docs show the target-tracking and scheduled options.
Step 4: SnapStart for Java, Python, and .NET
Lambda SnapStart takes a Firecracker snapshot of the initialized execution environment when you publish a version and restores from that snapshot on cold start, typically cutting init from seconds to well under a second. Originally Java-only, it has supported Python 3.12+ and .NET 8+ since late 2024. It is not available for Node.js; for Node, Provisioned Concurrency remains the tool.
functions:
report:
handler: app.handler
runtime: python3.13
snapStart: true
SnapStart caveats that bite in practice:
- Anything initialized before the snapshot is shared across restores: random seeds, unique IDs, and open network connections must be regenerated after restore. Use the runtime hooks (for Python, the
snapshot_restoredecorator) to reconnect. - Snapshot creation adds time to each deploy.
- There is a small per-restore charge and a snapshot storage cost on Python and .NET.
Step 5: Keep connections warm, not functions
"Warmer" plugins that ping your function every few minutes are a relic; they keep one environment warm while real traffic creates cold ones anyway, and they cost invocations. Remove serverless-plugin-warmup and similar from V3-era projects.
What does help is reusing expensive connections across warm invocations:
- Create SDK and database clients at module scope.
- Enable HTTP keep-alive (the default in SDK v3).
- For relational databases, use RDS Proxy or Aurora Data API rather than opening a connection per invocation.
Step 6: Move the cold start out of the request path
Sometimes the right fix is architectural:
- Put synchronous work behind SQS or EventBridge so the caller gets an immediate acknowledgment and the cold start happens asynchronously.
- For predictable bursts (a 9 a.m. login wave), schedule provisioned concurrency to ramp ten minutes before.
- For a genuinely latency-critical, high-throughput path, a container on Fargate or App Runner with no cold-start concept at all may be the honest answer.
A worked summary
A typical outcome from a tuning pass on a Node.js API: Power Tuning moves memory from 256 MB to 1024 MB (p95 init 1,400 ms to 600 ms, warm latency halved, cost down 20%); esbuild bundling and dropping aws-sdk v2 takes init to 250 ms; Provisioned Concurrency of 3 on the login endpoint during business hours removes the remaining visible cold starts for about the cost of a coffee a day. Three configuration changes, measured at each step.
Our Serverless Performance Tuning service runs exactly this sequence against your functions and hands you the numbers. Contact us if you would like it done.