Skip to content
All essays
ArchitectureMarch 20, 202513 min

Serverless Cold Starts: Understanding and Mitigation Strategies

Master serverless cold starts. Learn why they happen, how to measure them, and proven strategies to minimize their impact.

Ü
Ümit Uz
Mobile & Full Stack Developer

What Are Cold Starts?

A cold start occurs when a cloud provider needs to provision new resources to execute your serverless function. This includes loading your code, starting the runtime, and initializing dependencies before your function can begin processing.

typescript
// Cold Start Timeline
// 1. Request arrives
// 2. Allocate container (100-500ms)
// 3. Download function code (50-200ms)
// 4. Start runtime (Node.js/Python) (200-1000ms)
// 5. Load modules and dependencies (100-2000ms)
// 6. Execute initialization code (50-500ms)
// 7. Run handler code
// Total cold start: 500ms - 4+ seconds

Understanding Cold Start Triggers

When Cold Starts Occur

typescript
// Scenario 1: First deployment
// All functions start cold

// Scenario 2: No traffic for 5-30 minutes
// Container is reclaimed, next request is cold

// Scenario 3: Sudden traffic spike
// New containers spin up to handle load

// Scenario 4: Configuration changes
// Environment variables or memory changes

Cold Start Factors

typescript
// Language runtime choice
Node.js:    ~100-500ms  (Fast startup)
Python:     ~200-800ms  (Moderate)
Go:         ~50-200ms   (Very fast)
Java:       ~1000-3000ms (Slow)
.NET:       ~800-2000ms (Slow)

// Function package size
1MB:    ~200ms
10MB:   ~500ms
50MB:   ~2000ms
100MB+: ~4000ms+

// Memory allocation
128MB:  Slower execution
256MB:  Baseline
512MB:  Faster
1024MB+: Fastest (also more CPU)

// Dependency complexity
Minimal dependencies:  Fast
Heavy dependencies:    Slow
Database connections:  Variable

Measuring Cold Starts

AWS Lambda

typescript
import { Lambda } from '@aws-sdk/client-lambda';

const lambda = new Lambda({ region: 'us-east-1' });

async function measureColdStart(functionName: string) {
  const startTime = Date.now();

  const result = await lambda.invoke({
    FunctionName: functionName,
    InvocationType: 'RequestResponse',
  });

  const duration = Date.now() - startTime;

  // Check if this was a cold start
  const logs = await getLambdaLogs(functionName, startTime);
  const isColdStart = logs.includes('Init Duration');

  return {
    duration,
    isColdStart,
    initDuration: parseInitDuration(logs),
  };
}

// Track cold starts over time
const coldStarts: number[] = [];
const warmStarts: number[] = [];

for (let i = 0; i < 100; i++) {
  const result = await measureColdStart('my-function');

  if (result.isColdStart) {
    coldStarts.push(result.duration);
  } else {
    warmStarts.push(result.duration);
  }

  // Wait between invocations
  await new Promise(resolve => setTimeout(resolve, 1000));
}

console.log(`Cold starts: ${coldStarts.length}`);
console.log(`Warm starts: ${warmStarts.length}`);
console.log(`Avg cold start: ${average(coldStarts)}ms`);
console.log(`Avg warm start: ${average(warmStarts)}ms`);

CloudWatch Logs Analysis

typescript
import { CloudWatchLogs } from '@aws-sdk/client-cloudwatch-logs';

const logs = new CloudWatchLogs({ region: 'us-east-1' });

async function analyzeColdStarts(functionName: string) {
  const logGroup = `/aws/lambda/${functionName}`;

  const streams = await logs.describeLogStreams({
    logGroupName: logGroup,
    orderBy: 'LastEventTime',
    descending: true,
    limit: 100,
  });

  let coldStartCount = 0;
  let totalInitDuration = 0;

  for (const stream of streams.logStreams || []) {
    const events = await logs.getLogEvents({
      logGroupName: logGroup,
      logStreamName: stream.logStreamName,
    });

    for (const event of events.events || []) {
      const message = event.message || '';

      if (message.includes('Init Duration')) {
        coldStartCount++;

        const match = message.match(/Init Duration: ([d.]+) ms/);
        if (match) {
          totalInitDuration += parseFloat(match[1]);
        }
      }
    }
  }

  return {
    coldStartCount,
    avgInitDuration: totalInitDuration / coldStartCount,
  };
}

Optimization Strategies

1. Minimize Package Size

typescript
// ❌ Bad: Including all dependencies
import * as AWS from 'aws-sdk';
import * as moment from 'moment';
import * as _ from 'lodash';

// ✓ Good: Using specific imports
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { format } from 'date-fns';

// Use AWS SDK v3 (modular)
// Instead of entire AWS SDK (~50MB)
// Use only needed clients (~5MB)

// package.json
{
  "dependencies": {
    "@aws-sdk/client-dynamodb": "^3.0.0",
    "date-fns": "^2.0.0"
  },
  "devDependencies": {
    "aws-sdk": "^2.0.0" // Only for local development
  }
}

2. Optimize Initialization Code

typescript
// ❌ Bad: Heavy initialization
export const handler = async (event: any) => {
  // Connect to database on every invocation
  const db = await connectToDatabase();

  // Load heavy ML model
  const model = await loadMLModel();

  // Process request
  return processRequest(event, db, model);
};

// ✓ Good: Initialize outside handler
// Initialize connections once
const db = connectToDatabase();
const model = loadMLModel();

export const handler = async (event: any) => {
  // Reuse existing connections
  const connection = await db;

  return processRequest(event, connection, model);
};

3. Use Provisioned Concurrency

typescript
// AWS Lambda Provisioned Concurrency
// Keeps functions warm and ready

// Using AWS CDK
import { Function, Alias, Version } from 'aws-cdk-lib/aws-lambda';
import { CfnFunction } from 'aws-cdk-lib/aws-lambda';

const lambda = new Function(this, 'MyFunction', {
  runtime: Runtime.NODEJS_18_X,
  handler: 'index.handler',
  code: Code.fromAsset('lambda'),
});

// Create version
const version = lambda.currentVersion;

// Create alias
const alias = new Alias(this, 'ProvisionedAlias', {
  aliasName: 'provisioned',
  version,
});

// Configure provisioned concurrency
const cfnFunction = version.node.defaultChild as CfnFunction;
cfnFunction.addPropertyOverride('ProvisionedConcurrencyConfig', {
  ProvisionedConcurrentExecutions: 5, // Keep 5 instances warm
});

// Using Serverless Framework
# serverless.yml
functions:
  myFunction:
    handler: index.handler
    provisionedConcurrency: 5
    reservedConcurrentExecutions: 10

4. Implement Function Warming

typescript
// Create a warming function
import { EventBridgeScheduler } from '@aws-sdk/client-scheduler';

const scheduler = new EventBridgeScheduler({ region: 'us-east-1' });

async function setupWarmer(functionName: string) {
  // Schedule every 4 minutes (containers expire after 5)
  await scheduler.createSchedule({
    Name: `${functionName}-warmer`,
    ScheduleExpression: 'rate(4 minutes)',
    Target: {
      Arn: `arn:aws:lambda:${process.env.AWS_REGION}:${process.env.AWS_ACCOUNT_ID}:function:${functionName}`,
      RoleArn: process.env.WARMER_ROLE_ARN,
    },
  });
}

// Or use CloudWatch Events
import { CloudWatchEvents, CloudWatchEventsClient } from '@aws-sdk/client-cloudwatch-events';

const events = new CloudWatchEvents({ region: 'us-east-1' });

await events.putRule({
  Name: 'function-warmer',
  ScheduleExpression: 'rate(4 minutes)',
});

await events.putTargets({
  Rule: 'function-warmer',
  Targets: [{
    Id: '1',
    Arn: 'arn:aws:lambda:us-east-1:123456789012:function:my-function',
  }],
});

5. Optimize Memory Configuration

typescript
// Test different memory configurations
const memorySizes = [128, 256, 512, 1024, 2048];

for (const memory of memorySizes) {
  const results = [];

  for (let i = 0; i < 100; i++) {
    const result = await invokeFunction({
      FunctionName: 'test-function',
      MemorySize: memory,
    });

    results.push(result.duration);
  }

  console.log(`Memory: ${memory}MB`);
  console.log(`Avg duration: ${average(results)}ms`);
  console.log(`Cost: $${calculateCost(memory, average(results))}`);
}

// Find optimal balance
// More memory = faster execution but higher cost
// Usually 512MB-1024MB is optimal for Node.js

6. Use Lambda SnapStart (Java)

typescript
// For Java functions, use SnapStart
// Initializes snapshot of function

// Using SAM
// template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: MySnapStartFunction
      CodeUri: ./target/function.jar
      Handler: com.example.Handler::handleRequest
      Runtime: java11
      MemorySize: 2048
      Timeout: 30
      SnapStart:
        ApplyOn: PublishedVersions

Advanced Techniques

Database Connection Pooling

typescript
import { Pool } from 'pg';

// Create connection pool outside handler
const pool = new Pool({
  host: process.env.DB_HOST,
  port: 5432,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 10, // Maximum number of clients
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

export const handler = async (event: any) => {
  const client = await pool.connect();

  try {
    const result = await client.query('SELECT * FROM users');
    return { statusCode: 200, body: JSON.stringify(result.rows) };
  } finally {
    client.release(); // Release back to pool
  }
};

Lazy Loading Dependencies

typescript
// ❌ Bad: Load all modules at startup
import * as pdf from 'pdf-lib';
import * as excel from 'exceljs';
import * as image from 'sharp';

// ✓ Good: Lazy load
export const handler = async (event: any) => {
  const { type } = event;

  if (type === 'pdf') {
    const pdf = await import('pdf-lib');
    return processPdf(pdf);
  } else if (type === 'excel') {
    const excel = await import('exceljs');
    return processExcel(excel);
  }
};

Container Reuse Strategies

typescript
// Store state in global scope for reuse
let cache: Map<string, any> = new Map();
let lastRefresh = 0;
const CACHE_TTL = 60000; // 1 minute

export const handler = async (event: any) => {
  const now = Date.now();

  // Reuse cache if valid
  if (now - lastRefresh < CACHE_TTL) {
    const cached = cache.get(event.key);
    if (cached) return cached;
  }

  // Refresh cache
  const result = await fetchData(event.key);
  cache.set(event.key, result);
  lastRefresh = now;

  return result;
};

Monitoring and Alerting

CloudWatch Metrics

typescript
import { CloudWatch } from '@aws-sdk/client-cloudwatch';

const cloudwatch = new CloudWatch({ region: 'us-east-1' });

async function publishColdStartMetric(duration: number) {
  await cloudwatch.putMetricData({
    Namespace: 'Lambda/Performance',
    MetricData: [{
      MetricName: 'ColdStartDuration',
      Value: duration,
      Unit: 'Milliseconds',
      Timestamp: new Date(),
    }],
  });
}

// Create alarm for cold starts
await cloudwatch.putMetricAlarm({
  AlarmName: 'Lambda-ColdStart-Duration',
  AlarmDescription: 'Alert when cold starts exceed threshold',
  MetricName: 'ColdStartDuration',
  Namespace: 'Lambda/Performance',
  Statistic: 'Average',
  Period: 300,
  EvaluationPeriods: 1,
  Threshold: 3000, // 3 seconds
  ComparisonOperator: 'GreaterThanThreshold',
});

Distributed Tracing

typescript
import { Tracer } from '@aws-lambda-powertools/tracer';

const tracer = new Tracer({ serviceName: 'my-function' });

export const handler = tracer.captureLambdaHandler(async (event: any) => {
  // Trace cold start
  if (process.env.AWS_LAMBDA_INITIALIZATION_TYPE === 'on-demand') {
    tracer.putAnnotation('ColdStart', true);
  }

  // Trace execution time
  const subsegment = tracer.getSegment()?.addNewSubsegment('processing');

  try {
    const result = await processEvent(event);
    return result;
  } finally {
    subsegment?.close();
  }
});

Cost Optimization

Calculate Cold Start Impact

typescript
function calculateColdStartCost(
  invocations: number,
  coldStartRate: number,
  avgColdStartDuration: number,
  avgWarmStartDuration: number,
  memorySize: number
) {
  const coldStarts = invocations * coldStartRate;
  const warmStarts = invocations * (1 - coldStartRate);

  // Calculate duration (including cold start time)
  const totalDuration = (coldStarts * avgColdStartDuration) +
                       (warmStarts * avgWarmStartDuration);

  // Lambda pricing (us-east-1)
  const pricePerMs = 0.00000001667 * (memorySize / 128);
  const requestPrice = invocations * 0.0000002;

  const durationPrice = totalDuration * pricePerMs;
  const totalPrice = requestPrice + durationPrice;

  return {
    coldStartCost: coldStarts * avgColdStartDuration * pricePerMs,
    warmCost: warmStarts * avgWarmStartDuration * pricePerMs,
    totalCost: totalPrice,
  };
}

// Example
const cost = calculateColdStartCost(
  1000000,    // 1M invocations
  0.2,        // 20% cold start rate
  2000,       // 2s average cold start
  100,        // 100ms average warm start
  512         // 512MB memory
);

console.log(`Cold start cost: $${cost.coldStartCost.toFixed(2)}`);
console.log(`Total cost: $${cost.totalCost.toFixed(2)}`);

Best Practices

1. Choose the Right Runtime

typescript
// Fastest cold starts: Go, Rust
// Good balance: Node.js, Python
// For Java/.NET: Use SnapStart or GraalVM

2. Optimize Package Size

bash
# Use webpack/bundle for Node.js
npm install --production
webpack --mode production

# Exclude dev dependencies
serverless package --exclude devDependencies

# Use layers for common dependencies

3. Monitor and Tune

typescript
// Regularly review cold start metrics
// Adjust provisioned concurrency based on traffic patterns
// Test changes before deploying to production

4. Use Appropriate Timeouts

typescript
// Set timeouts to account for cold starts
const timeout = max(functionDuration, coldStartDuration) + buffer;

// Example: If function takes 5s and cold start takes 2s
// Set timeout to at least 10s

Conclusion

Cold starts are an inherent characteristic of serverless computing, but with the right strategies, their impact can be minimized. By understanding what causes cold starts and implementing proven optimization techniques, you can build serverless applications that are both cost-effective and performant.

Key takeaways:

  • Measure cold starts to understand their impact
  • Optimize package size and initialization code
  • Use provisioned concurrency for critical functions
  • Implement warming strategies for predictable performance
  • Monitor and tune based on actual usage patterns

Master cold starts and deliver responsive serverless applications!

Enjoyed this essay? Share it.

Related essays

Next essay
VS Code Extensions Development: Building Custom Tools