Skip to content

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.

3 min read
Cold start latency spike visible in serverless function response time graph

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.

tstypescript
// 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.

tstypescript
// ❌ 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.

tstypescript
// ❌ 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:

shbash
# 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 runtime

Mitigation Strategy 2: Lazy Initialization

Not every invocation needs every dependency. Initialize expensive resources only when the code path requires them.

tstypescript
// ❌ 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.

ymlyaml
# 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: ANY

Mitigation Strategy 4: Memory and Runtime Selection

More memory allocation proportionally increases CPU, which speeds up initialization.

tstypescript
// 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 tolerance

VPC 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

  1. Cold starts are initialization overhead — container provisioning, runtime startup, and module loading all contribute
  2. Reduce package size — bundle and tree-shake your dependencies, use modular SDK imports
  3. Lazy-initialize expensive resources — only create clients for code paths that need them
  4. Provisioned concurrency eliminates cold starts — at the cost of paying for always-on compute
  5. More memory means faster initialization — the CPU scales linearly with memory allocation
  6. Avoid VPCs unless necessary — VPC attachment still adds measurable cold start overhead
Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX