Lambda cold starts are the #1 performance complaint in serverless architectures. When a Lambda function hasn't been invoked recently — or when concurrent invocations exceed available warm instances — AWS must spin up a new execution environment from scratch. That initialization penalty can add 100ms to 10+ seconds to your response time, depending on runtime, package size, and VPC configuration.

Here's the good news: cold starts are a solved problem in 2026. Not with one silver bullet, but with a combination of techniques that together can reduce cold start impact to near-zero for most workloads.

This guide covers 9 proven optimization techniques with real numbers, code examples, and cost trade-offs.

What Causes Cold Starts

Every Lambda cold start goes through the same lifecycle:

  1. Download code — AWS fetches your deployment package from S3 (or container image from ECR)
  2. Start runtime — Initialize the language runtime (Python interpreter, Node.js V8, JVM, etc.)
  3. Run init code — Execute any code outside your handler function (imports, DB connections, SDK clients)
  4. Execute handler — Finally run your actual function logic

Steps 1–3 only happen on cold starts. Warm invocations skip straight to step 4.

The init phase is where the real cost hides. A Java function importing Spring Boot can spend 3–8 seconds in step 3 alone. A Python function with numpy takes ~500ms. A Go function with minimal dependencies? Under 50ms.

Measuring Cold Starts: X-Ray Traces

Before optimizing, you need to measure. Enable AWS X-Ray tracing on your Lambda functions to see the actual cold vs warm breakdown:

aws lambda update-function-configuration \
  --function-name my-function \
  --tracing-config Mode=Active

In the X-Ray console, cold start traces show a distinct "Initialization" segment before the "Invocation" segment. Warm traces only show "Invocation."

Key metrics to track:

  • Init Duration — reported in CloudWatch Logs as Init Duration: X ms
  • P99 latency — cold starts are rare but hit your worst-case users
  • Cold start percentage — what fraction of invocations are cold?

Cold Start Times by Runtime

Real-world cold start durations for a minimal "hello world" function (128MB memory, us-east-1, 2026):

RuntimeMedian Cold StartP99 Cold StartNotes
Go 1.x (provided.al2023)~35ms~80msFastest runtime, compiled binary
Python 3.12~100ms~250msFast init, huge ecosystem
Node.js 20.x~180ms~400msV8 JIT adds overhead
Ruby 3.3~280ms~500msModerate, rarely used
.NET 8 (AOT)~200ms~450msAOT compilation helps hugely
.NET 8 (managed)~500ms~1200msCLR init is expensive
Java 21 (no SnapStart)~1000ms~3500msJVM + classpath scanning
Java 21 (with SnapStart)~100ms~300msGame-changer — see Technique 4

These numbers increase significantly with package size, dependencies, and VPC configuration.

Technique 1: Choose the Right Runtime

The simplest cold start optimization: pick a faster runtime.

If cold start latency is critical (API Gateway → Lambda user-facing APIs), prefer:

  1. Go — compiled, no runtime overhead, ~35ms cold starts
  2. Python — fast interpreter init, ~100ms cold starts
  3. Node.js — reasonable at ~180ms, huge ecosystem

Avoid for latency-sensitive paths:

  • Java (without SnapStart) — 1–3.5 seconds cold starts
  • .NET managed — 500ms+ cold starts

This doesn't mean "never use Java." It means don't put Java Lambda behind a synchronous API Gateway endpoint that users are waiting on. For async event processing (SQS, S3 triggers, Step Functions), cold starts don't matter.

Technique 2: Reduce Package Size

Smaller packages download faster (step 1) and have less code to initialize (step 3).

Target: under 50MB for fast cold starts. Under 10MB is ideal.

Node.js: Tree-shaking with esbuild

// esbuild.config.ts
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/*'], // AWS SDK v3 is included in the Lambda runtime
  treeShaking: true,
});

Before: 45MB node_modules → After: 2MB bundled. Cold start drops from ~400ms to ~150ms.

Key insight: AWS SDK v3 is pre-installed in the Lambda runtime. Don't bundle it — mark it as external.

Python: Minimize dependencies

# Use Lambda Layers for heavy packages
aws lambda publish-layer-version \
  --layer-name numpy-layer \
  --zip-file fileb://layer.zip \
  --compatible-runtimes python3.12

# In your function, only import what you need
# BAD: import boto3 (imports entire SDK)
# GOOD: from boto3 import client (slightly better)
# BEST: import boto3; s3 = boto3.client('s3') (lazy, in init)

General tips:

  • Remove test files, docs, and type stubs from deployment packages
  • Use .dockerignore or .lambdaignore patterns
  • Audit with du -sh node_modules/* or pip show --files
  • Consider Lambda Layers for shared heavy dependencies (numpy, pandas, Pillow)

Technique 3: Provisioned Concurrency

When you absolutely cannot tolerate cold starts — even occasionally — Provisioned Concurrency keeps a specified number of Lambda instances warm at all times.

aws lambda put-provisioned-concurrency-config \
  --function-name my-api-handler \
  --qualifier my-alias \
  --provisioned-concurrent-executions 10

Cost: $0.015 per GB-hour of provisioned concurrency (on top of normal invocation costs).

Math example:

  • Function: 512MB memory, 10 provisioned instances
  • Cost: 10 × 0.5 GB × $0.015/GB-hr × 730 hrs = $54.75/month
  • Compare to: 10,000 cold starts/month × 2s each = 20,000s of user-facing latency eliminated

When it makes sense:

  • User-facing APIs with strict latency SLAs (<200ms P99)
  • Functions that process real-time events (Kinesis, DynamoDB Streams)
  • High-traffic functions where cold start % is already low (provisioned concurrency just eliminates the remaining 1–5%)

When it doesn't make sense:

  • Async processing (SQS, S3 triggers) — users aren't waiting
  • Dev/staging environments
  • Functions with <100 invocations/day (keep-warm trick is cheaper — see Technique 5)

Auto-scaling Provisioned Concurrency

Use Application Auto Scaling to adjust provisioned concurrency based on utilization:

aws application-autoscaling register-scalable-target \
  --service-namespace lambda \
  --resource-id function:my-api-handler:my-alias \
  --scalable-dimension lambda:function:ProvisionedConcurrency \
  --min-capacity 5 \
  --max-capacity 50

aws application-autoscaling put-scaling-policy \
  --service-namespace lambda \
  --resource-id function:my-api-handler:my-alias \
  --scalable-dimension lambda:function:ProvisionedConcurrency \
  --policy-name target-tracking \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{
    "TargetValue": 0.7,
    "PredefinedMetricSpecification": {
      "PredefinedMetricType": "LambdaProvisionedConcurrencyUtilization"
    }
  }'

This keeps provisioned concurrency at ~70% utilization — enough headroom for spikes without over-provisioning.

Technique 4: Lambda SnapStart (Java 11+)

SnapStart is AWS's answer to Java cold starts. It takes a snapshot of the initialized execution environment after your init code runs, then restores from that snapshot on cold starts instead of re-initializing.

Result: 90% cold start reduction for Java functions. A function that took 3 seconds to cold start now takes ~300ms.

How to enable:

aws lambda update-function-configuration \
  --function-name my-java-function \
  --snap-start ApplyOn=PublishedVersions

# Publish a new version to activate SnapStart
aws lambda publish-version \
  --function-name my-java-function

CDK example:

const fn = new lambda.Function(this, 'MyJavaFunction', {
  runtime: lambda.Runtime.JAVA_21,
  handler: 'com.example.Handler::handleRequest',
  code: lambda.Code.fromAsset('target/my-function.jar'),
  memorySize: 1024,
  snapStart: lambda.SnapStartConf.ON_PUBLISHED_VERSIONS,
});

// You MUST publish a version for SnapStart to work
const version = fn.currentVersion;

Caveats:

  • Only works with Java 11+ (Corretto) runtimes
  • Must use published versions (not $LATEST)
  • Uniqueness concerns: cached random values, connection state, and temp files are restored from the snapshot. Use CRaC hooks to handle:
import org.crac.Context;
import org.crac.Core;
import org.crac.Resource;

public class Handler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent>, Resource {
    public Handler() {
        Core.getGlobalContext().register(this);
    }

    @Override
    public void afterRestore(Context<? extends Resource> context) {
        // Re-initialize connections, refresh credentials, reseed RNG
        this.dbConnection = createNewConnection();
    }
}

Technique 5: Keep-Warm with Scheduled EventBridge

The simplest (and cheapest) way to avoid cold starts for low-traffic functions: ping them on a schedule.

EventBridge rule (every 5 minutes):

aws events put-rule \
  --name keep-warm-my-function \
  --schedule-expression "rate(5 minutes)"

aws events put-targets \
  --rule keep-warm-my-function \
  --targets '[{"Id":"1","Arn":"arn:aws:lambda:us-east-1:123456789:function:my-function","Input":"{\"source\":\"warmup\"}"}]'

In your handler, return immediately for warmup events:

export const handler = async (event: any) => {
  // Short-circuit warmup invocations
  if (event.source === 'warmup') {
    return { statusCode: 200, body: 'warm' };
  }

  // Actual business logic
  return processRequest(event);
};

Terraform example:

resource "aws_cloudwatch_event_rule" "keep_warm" {
  name                = "keep-warm-my-function"
  schedule_expression = "rate(5 minutes)"
}

resource "aws_cloudwatch_event_target" "keep_warm" {
  rule      = aws_cloudwatch_event_rule.keep_warm.name
  target_id = "keep-warm"
  arn       = aws_lambda_function.my_function.arn
  input     = jsonencode({ source = "warmup" })
}

resource "aws_lambda_permission" "allow_eventbridge" {
  statement_id  = "AllowEventBridge"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.my_function.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.keep_warm.arn
}

Cost: EventBridge: free (under 14M events/month free tier). Lambda: ~288 invocations/day × minimal duration = pennies/month.

Limitation: This only keeps ONE instance warm. If you need N concurrent warm instances, you need to invoke the function N times concurrently (use Step Functions or a custom warmer).

Technique 6: Connection Reuse — Init Outside the Handler

Any code outside your handler function runs once during cold start and persists across warm invocations. Use this to initialize expensive resources:

// ✅ GOOD: Initialize outside handler — runs once, reused across invocations
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

// Database connection pool — created once, reused
let dbPool: Pool | null = null;
async function getPool() {
  if (!dbPool) {
    dbPool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 1, // Lambda = 1 concurrent request per instance
    });
  }
  return dbPool;
}

export const handler = async (event: any) => {
  const pool = await getPool();
  // Use pool — connection is already established from previous invocations
};
# ✅ GOOD: Python equivalent
import boto3
import psycopg2
import os

# These run ONCE on cold start, then persist
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])

# Lazy connection init
_conn = None
def get_conn():
    global _conn
    if _conn is None or _conn.closed:
        _conn = psycopg2.connect(os.environ['DATABASE_URL'])
    return _conn

def handler(event, context):
    conn = get_conn()
    # Use existing connection

Key rules:

  • AWS SDK clients: always init outside handler
  • Database connections: lazy-init with connection check (connections can timeout)
  • Set max: 1 for connection pools — each Lambda instance handles one request at a time
  • Enable HTTP keep-alive for SDK clients: AWS_NODEJS_CONNECTION_REUSE_ENABLED=1

Technique 7: ARM64 / Graviton2

Lambda functions running on ARM64 (Graviton2) processors are both cheaper and have faster cold starts than x86_64:

  • 20% cheaper per millisecond of compute
  • 10–20% faster cold starts due to better per-core performance
  • Same or better warm performance for most workloads
aws lambda update-function-configuration \
  --function-name my-function \
  --architectures arm64

CDK:

new lambda.Function(this, 'MyFunction', {
  runtime: lambda.Runtime.NODEJS_20_X,
  architecture: lambda.Architecture.ARM_64,
  // ... rest of config
});

Compatibility: Most Lambda runtimes and packages work without changes on ARM64. The exceptions:

  • Native binaries compiled for x86 (C extensions, Rust FFI)
  • Some older npm packages with native addons
  • Test on ARM64 in staging before deploying to production

For Python and Node.js pure-JS workloads, switching to ARM64 is a free performance + cost win.

Technique 8: Memory Size Tuning

Lambda allocates CPU proportionally to memory. More memory = more CPU = faster cold starts (and warm execution).

The counterintuitive truth: increasing memory from 128MB to 512MB often reduces total cost because the function runs faster.

MemoryCold StartWarm DurationCost per Invocation
128 MB~350ms~200ms$0.0000033
256 MB~250ms~120ms$0.0000040
512 MB~180ms~70ms$0.0000047
1024 MB~140ms~45ms$0.0000060
1769 MB (1 vCPU)~120ms~35ms$0.0000083

Use the AWS Lambda Power Tuning tool to find the optimal memory setting:

# Deploy the power tuning state machine (one-time)
# https://github.com/alexcasalboni/aws-lambda-power-tuning

# Run it against your function
aws stepfunctions start-execution \
  --state-machine-arn arn:aws:states:us-east-1:123456789:stateMachine:powerTuningStateMachine \
  --input '{
    "lambdaARN": "arn:aws:lambda:us-east-1:123456789:function:my-function",
    "powerValues": [128, 256, 512, 1024, 1769, 3008],
    "num": 50,
    "payload": "{}",
    "parallelInvocation": true
  }'

The tool generates a visualization showing the cost-performance curve. Most functions have a "sweet spot" between 512MB and 1024MB.

Technique 9: VPC Avoidance

Historically, Lambda functions inside a VPC added 5–10 seconds of cold start latency due to ENI (Elastic Network Interface) creation. AWS improved this significantly with Hyperplane ENIs in 2019, but VPC cold starts still add 200–500ms overhead in 2026.

If your function doesn't need VPC access, don't put it in a VPC.

Functions that do NOT need VPC:

  • Functions calling only AWS services (DynamoDB, S3, SQS, SNS) — use IAM roles + service endpoints
  • Functions calling public APIs
  • Functions processing S3 events, CloudWatch events, API Gateway requests

Functions that DO need VPC:

  • Functions connecting to RDS, ElastiCache, or other VPC-only resources
  • Functions accessing private APIs or on-prem resources via VPN/Direct Connect

If you must use VPC, minimize the cold start impact:

  1. Use VPC Endpoints for AWS services to avoid NAT Gateway (which adds its own latency):
aws ec2 create-vpc-endpoint \
  --vpc-id vpc-xxx \
  --service-name com.amazonaws.us-east-1.dynamodb \
  --route-table-ids rtb-xxx
  1. Use RDS Proxy for database connections — reduces connection overhead and handles Lambda's ephemeral connection pattern:
const pool = new Pool({
  host: process.env.RDS_PROXY_ENDPOINT, // RDS Proxy, not direct RDS
  max: 1,
  ssl: { rejectUnauthorized: false },
});
  1. Consider VPC Lattice for service-to-service communication within a VPC — avoids the need for complex networking.

Measuring Results: CloudWatch Lambda Insights

After applying optimizations, measure the impact:

# Enable Lambda Insights
aws lambda update-function-configuration \
  --function-name my-function \
  --layers arn:aws:lambda:us-east-1:580247275435:layer:LambdaInsightsExtension:49

Lambda Insights provides:

  • Init duration — cold start time specifically
  • Memory utilization — are you over-provisioning?
  • Cold start frequency — percentage of invocations that are cold
  • CPU time vs wall clock time — detect I/O bottlenecks

Custom CloudWatch metric for cold start tracking:

import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch';

const cw = new CloudWatchClient({});
let isColdStart = true;

export const handler = async (event: any) => {
  if (isColdStart) {
    await cw.send(new PutMetricDataCommand({
      Namespace: 'MyApp/Lambda',
      MetricData: [{
        MetricName: 'ColdStart',
        Value: 1,
        Unit: 'Count',
        Dimensions: [{ Name: 'FunctionName', Value: process.env.AWS_LAMBDA_FUNCTION_NAME! }],
      }],
    }));
    isColdStart = false;
  }
  // ... handler logic
};

Decision Matrix: Which Technique for Each Scenario

ScenarioBest TechniquesExpected Impact
User-facing API, strict latencyProvisioned Concurrency + ARM64 + memory tuningNear-zero cold starts
Java/Spring Boot functionsSnapStart + memory ≥1024MB90% cold start reduction
Low-traffic function (<100/day)Keep-warm EventBridge + connection reuseEliminates most cold starts
Node.js API with large node_modulesesbuild bundling + tree-shaking + ARM6450–70% cold start reduction
Python data processingLayers for heavy deps + memory tuning30–50% reduction
Any function in VPCVPC Endpoints + RDS Proxy + memory tuning200–500ms saved
Async event processing (SQS/S3)None needed — cold starts don't impact UXN/A
Cost-sensitive, moderate trafficKeep-warm + ARM64 + right-size memoryBest ROI for minimal spend

The Bottom Line

Cold starts aren't a reason to avoid serverless. They're a tuning knob — and in 2026, you have more knobs than ever.

Start with the free optimizations: ARM64, package size reduction, connection reuse, and memory tuning. These cost nothing and benefit every invocation, not just cold starts.

If you still need sub-100ms P99, add Provisioned Concurrency for your critical paths. For Java, SnapStart is non-negotiable.

The goal isn't zero cold starts everywhere. It's zero cold starts where they matter — user-facing synchronous paths. Let the async workloads cold-start freely.


Use the Serverless Cost Calculator to see how Lambda costs compare to Azure Functions, GCP Cloud Functions, and Cloudflare Workers for your workload.