If you’ve ever deployed a serverless function and watched your first request crawl at 3-5 seconds, you’ve met the cold start problem. In 2026, AWS Lambda has made significant improvements, but cold starts still bite developers who don’t plan for them. This guide covers practical, tested strategies to get your Lambda functions responding in under 100 milliseconds — consistently.
What Causes Cold Starts in 2026?
A cold start happens when AWS needs to provision a new execution environment for your function. This involves downloading your deployment package, initializing the runtime, and running your initialization code. The total delay depends on three factors:
- Runtime choice — Python and Node.js start faster than Java or .NET
- Package size — Larger deployment packages take longer to download and extract
- Initialization code — Database connections, SDK clients, and config loading all add up
In 2026, Lambda SnapStart (now available for Python and Node.js, not just Java) has changed the game, but it’s not a silver bullet. Let’s look at what actually works.
Strategy 1: Use Lambda SnapStart for Python and Node.js
AWS extended SnapStart to Python 3.12+ and Node.js 20+ runtimes in late 2025. It takes a snapshot of your initialized execution environment and restores it on cold starts instead of re-running init code.
# serverless.yml configuration
functions:
api:
handler: handler.main
runtime: python3.13
snapStart: true
environment:
POWERTOOLS_SERVICE_NAME: my-apiWith SnapStart enabled, a Python function that previously took 1.8 seconds to cold start typically drops to 200-400ms. But to get under 100ms, you need to combine it with other strategies.
SnapStart Gotchas
Be careful with connections and randomness. Since the snapshot is taken after init, any connections established during init will be stale when the snapshot restores:
import boto3
from functools import lru_cache
# BAD: Connection created at init, will be stale after restore
# db = psycopg2.connect(os.environ['DB_URL'])
# GOOD: Lazy initialization with connection validation
@lru_cache(maxsize=1)
def get_db():
conn = psycopg2.connect(os.environ['DB_URL'])
return conn
def handler(event, context):
db = get_db()
try:
db.execute("SELECT 1") # Validate connection
except Exception:
get_db.cache_clear()
db = get_db()
# ... rest of your logicStrategy 2: Minimize Package Size with Lambda Layers and Tree Shaking
Every megabyte in your deployment package adds roughly 30-50ms to cold start time. Here’s how to trim the fat:
For Python: Use Lambda-Optimized Packages
# Instead of including boto3 (it's already in the runtime)
# requirements.txt — only include what's NOT in the runtime
fastapi==0.115.0
mangum==0.19.0
pydantic==2.10.0
# Build with --platform to avoid unnecessary binaries
pip install -r requirements.txt \
--platform manylinux2014_aarch64 \
--target ./package \
--only-binary=:all: \
--no-cache-dirFor Node.js: Bundle with esbuild
// build.mjs
import { build } from 'esbuild';
await build({
entryPoints: ['src/handler.ts'],
bundle: true,
minify: true,
platform: 'node',
target: 'node20',
outfile: 'dist/handler.js',
external: ['@aws-sdk/*'], // Available in runtime
treeShaking: true,
});This typically reduces a Node.js package from 50MB+ down to under 1MB, cutting cold starts by 60-70%.
Strategy 3: Use ARM64 (Graviton) Architecture
Graviton-based Lambda functions aren’t just 20% cheaper — they consistently show 10-15% faster cold starts compared to x86_64:
# Switch to ARM64
functions:
api:
handler: handler.main
architecture: arm64 # This one line saves money AND time
runtime: python3.13
memorySize: 512Strategy 4: Right-Size Memory (It Affects CPU Too)
Lambda allocates CPU proportionally to memory. At 128MB, you get a fraction of a vCPU. At 1769MB, you get exactly one full vCPU. More memory = more CPU = faster initialization:
// Use AWS Lambda Power Tuning to find the sweet spot
// https://github.com/alexcasalboni/aws-lambda-power-tuning
// Typical results for an API function:
// 128MB → Cold start: 2100ms, Cost: $0.000002
// 512MB → Cold start: 680ms, Cost: $0.000004
// 1024MB → Cold start: 340ms, Cost: $0.000008
// 1536MB → Cold start: 290ms, Cost: $0.000012
// Sweet spot is usually 512-1024MB for most workloadsStrategy 5: Provisioned Concurrency for Critical Paths
When you absolutely cannot tolerate cold starts — payment processing, real-time APIs, authentication — use provisioned concurrency:
# Keep 5 instances warm at all times
functions:
checkout:
handler: handler.checkout
provisionedConcurrency: 5
events:
- httpApi:
path: /checkout
method: postThe cost is roughly $15/month per provisioned instance (at 512MB). For critical endpoints, this is insurance worth paying for.
Smart Scheduling with Application Auto Scaling
# Scale provisioned concurrency based on schedule
aws application-autoscaling put-scheduled-action \
--service-namespace lambda \
--resource-id function:checkout:prod \
--scheduled-action-name business-hours \
--schedule "cron(0 8 ? * MON-FRI *)" \
--scalable-dimension lambda:function:ProvisionedConcurrency \
--scalable-target-action MinCapacity=10,MaxCapacity=50Strategy 6: Extension-Free or Extension-Light
Lambda extensions (monitoring agents, secret managers) each add 50-200ms to cold starts. Audit what you’re running:
# Check what extensions are loaded
ls /opt/extensions/
# Common offenders:
# - Datadog agent: ~150ms
# - Secrets Manager extension: ~100ms
# - AppConfig extension: ~80ms
# Alternative: Use Parameter Store with caching
import os
from aws_lambda_powertools.utilities import parameters
# Cached for 5 minutes, loaded lazily
def get_config():
return parameters.get_parameter(
"/myapp/config",
max_age=300,
transform="json"
)Putting It All Together: A Real-World Example
Here’s a FastAPI function that implements all six strategies:
# handler.py
from mangum import Mangum
from fastapi import FastAPI
from contextlib import asynccontextmanager
app = FastAPI()
# Lazy-loaded resources (SnapStart compatible)
_resources = {}
def get_table():
if 'table' not in _resources:
import boto3
_resources['table'] = boto3.resource(
'dynamodb'
).Table('my-table')
return _resources['table']
@app.get("/items/{item_id}")
async def get_item(item_id: str):
result = get_table().get_item(Key={'id': item_id})
return result.get('Item', {})
handler = Mangum(app, lifespan="off")Results with all optimizations combined:
- Cold start without optimizations: 2,400ms
- + SnapStart: 380ms
- + ARM64 + right-sized memory (512MB): 220ms
- + Bundled/trimmed package: 95ms
- + Provisioned concurrency: 0ms (no cold start)
Measuring Cold Starts
You can’t optimize what you don’t measure. Use CloudWatch Insights to track cold starts:
filter @type = "REPORT"
| fields @requestId, @duration, @billedDuration, @initDuration
| filter ispresent(@initDuration)
| stats count() as coldStarts,
avg(@initDuration) as avgColdStart,
pct(@initDuration, 99) as p99ColdStart
by bin(1h)The Bottom Line
Cold starts in 2026 are more manageable than ever, but they still require intentional architecture decisions. Start with SnapStart and package optimization — these are free and give you the biggest wins. Add provisioned concurrency only for the endpoints where latency truly matters. And always measure: what gets measured gets improved.

Leave a Reply