Serverless Cold Starts: Causes, Costs, and Mitigation
Cold starts are the hidden tax of serverless — understand what causes them, how much they cost in latency, and practical strategies to keep functions warm.

The first invocation of a serverless function after a period of inactivity takes significantly longer than subsequent calls. This "cold start" includes provisioning a container, loading the runtime, initializing dependencies, and running your initialization code. On AWS Lambda, cold starts can add 100ms to several seconds depending on runtime choice, package size, and VPC configuration.
What Happens During a Cold Start
A serverless invocation follows this path: the platform receives the request, checks for a warm container, and if none exists, provisions a new one. The cold path includes downloading the deployment package, starting the runtime, and executing module-level code before the handler can process the request.
// Everything outside the handler runs during cold start
import { DynamoDB } from "@aws-sdk/client-dynamodb"; // ~200ms import
import { S3Client } from "@aws-sdk/client-s3"; // ~150ms import
const dynamodb = new DynamoDB({}); // Connection initialization
const s3 = new S3Client({});
// This handler runs on EVERY invocation (warm or cold)
export const handler = async (event: APIGatewayEvent) => {
// Handler code here
return { statusCode: 200, body: "OK" };
};Measuring Cold Start Impact
The difference between cold and warm responses is measurable with structured logging.
// ❌ No visibility into cold starts
export const handler = async (event: unknown) => {
const result = await processRequest(event);
return result;
};
// ✅ Track cold starts explicitly
let isFirstInvocation = true;
export const handler = async (event: unknown) => {
const start = Date.now();
const isCold = isFirstInvocation;
isFirstInvocation = false;
const result = await processRequest(event);
console.log(JSON.stringify({
coldStart: isCold,
duration: Date.now() - start,
functionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
memoryAllocated: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE,
}));
return result;
};Mitigation Strategy 1: Reduce Package Size
The deployment package must be downloaded and extracted before the runtime starts. Smaller packages mean faster cold starts.
// ❌ Importing the entire AWS SDK v2 (~70MB)
import AWS from "aws-sdk";
const dynamodb = new AWS.DynamoDB();
// ✅ Import only what you need (SDK v3 modular imports ~3MB)
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({});Tree-shaking and bundling your function code with esbuild or similar tools can drastically reduce package size:
# Bundle with esbuild — output is a single file with only used code
npx esbuild src/handler.ts \
--bundle \
--platform=node \
--target=node18 \
--outfile=dist/handler.js \
--minify \
--external:@aws-sdk/* # SDK v3 is included in Lambda runtimeMitigation Strategy 2: Lazy Initialization
Not every invocation needs every dependency. Initialize expensive resources only when the code path requires them.
// ❌ Always initializing S3 even if most invocations don't use it
const s3 = new S3Client({});
const dynamodb = new DynamoDBClient({});
export const handler = async (event: APIGatewayEvent) => {
if (event.path === "/upload") {
await s3.send(new PutObjectCommand(uploadParams));
}
// 90% of invocations only use DynamoDB
return dynamodb.send(new GetItemCommand(params));
};
// ✅ Lazy initialization — S3 client created only when needed
const dynamodb = new DynamoDBClient({});
let _s3: S3Client | null = null;
function getS3Client(): S3Client {
if (!_s3) _s3 = new S3Client({});
return _s3;
}
export const handler = async (event: APIGatewayEvent) => {
if (event.path === "/upload") {
await getS3Client().send(new PutObjectCommand(uploadParams));
}
return dynamodb.send(new GetItemCommand(params));
};Mitigation Strategy 3: Provisioned Concurrency
For latency-sensitive paths, provisioned concurrency keeps a specified number of function instances warm at all times — eliminating cold starts entirely at the cost of paying for idle compute.
# serverless.yml — provisioned concurrency configuration
functions:
api:
handler: dist/handler.handler
memorySize: 512
provisionedConcurrency: 5 # 5 instances always warm
events:
- http:
path: /api/{proxy+}
method: ANYMitigation Strategy 4: Memory and Runtime Selection
More memory allocation proportionally increases CPU, which speeds up initialization.
// CloudFormation / SAM template
// Doubling memory from 128MB to 256MB can cut cold start time in half
// The extra cost per invocation is often offset by faster execution
// Runtime comparison (approximate cold start overhead):
// Python 3.x: ~200ms
// Node.js 18.x: ~250ms
// Go 1.x: ~100ms (compiled binary)
// Java 17: ~800ms (JVM startup)
// .NET 6: ~400ms
// Choose your runtime based on cold start toleranceVPC Cold Starts
Attaching a Lambda function to a VPC historically added 5-10 seconds to cold starts due to ENI (Elastic Network Interface) creation. AWS has improved this with Hyperplane ENI, but VPC functions still have higher cold start latency. Only attach to a VPC when the function actually needs to access VPC resources.
Key Takeaways
- Cold starts are initialization overhead — container provisioning, runtime startup, and module loading all contribute
- Reduce package size — bundle and tree-shake your dependencies, use modular SDK imports
- Lazy-initialize expensive resources — only create clients for code paths that need them
- Provisioned concurrency eliminates cold starts — at the cost of paying for always-on compute
- More memory means faster initialization — the CPU scales linearly with memory allocation
- Avoid VPCs unless necessary — VPC attachment still adds measurable cold start overhead


