Custom Runtime Cold Start Optimization
AWS Lambda cold starts can kill application performance. Here’s how we achieved 90% faster cold starts using custom runtimes and shell scripts.
Understanding Lambda Cold Starts
A cold start happens when AWS Lambda creates a new execution environment for your function. This includes:
- Environment setup - downloading your code
- Runtime initialization - starting the language runtime
- Function initialization - loading your code and dependencies
Standard runtimes like Node.js, Python, and Java have significant initialization overhead. Custom runtimes let you optimize this process.
Cold Start Performance Comparison
Real Production Metrics
Measured against the same travel booking API across three runtimes:
| Runtime | Init Duration | Duration | Memory |
|---|---|---|---|
| Node.js 18.x | 247.82ms | 1,240ms | 152MB |
| Python 3.11 | 189.45ms | 890ms | 128MB |
| Shell (Go bootstrap) | 22.11ms | 316ms | 36MB |
Results:
- 90% faster cold starts vs Node.js
- 88% faster cold starts vs Python
- 75% less memory usage
- Significantly lower costs
For the full cold start optimization journey including the progression from pure-bash (~90ms) to Go bootstrap (~21ms), see the complete benchmark article.
Why Custom Runtimes Are Faster
1. Minimal Bootstrap Process
Standard runtimes initialize entire language environments. Custom runtimes start with just what you need:
#!/bin/bash
# Custom runtime bootstrap - 20 lines total
while true; do
EVENT=$(curl -sS "$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/next")
RESPONSE=$(./handler.sh "$EVENT")
curl -sS "$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/$REQUEST_ID/response" \
-d "$RESPONSE"
done
2. No Dependency Loading
Node.js loads npm modules, Python imports packages. Shell scripts use pre-compiled binaries:
# These are already compiled C binaries
curl -sS "https://api.example.com/data" | jq '.results[]'
3. Smaller Image Sizes
Our custom runtime images:
- tiny: 132MB (jq, curl, http-cli)
- micro: 221MB (adds AWS tools)
- full: 417MB (complete AWS CLI)
Compare to standard runtimes:
- Node.js: 500MB+
- Python: 400MB+
- Java: 800MB+
Lambda Cold Start Optimization Techniques
1. Use Provisioned Concurrency Strategically
For critical functions, provisioned concurrency eliminates cold starts entirely:
resource "aws_lambda_provisioned_concurrency_config" "example" {
function_name = aws_lambda_function.example.function_name
provisioned_concurrent_executions = 5
qualifier = aws_lambda_function.example.version
}
Cost consideration: Only use for high-traffic, latency-sensitive functions.
2. Optimize Memory Allocation
More memory = faster CPU = faster initialization:
resource "aws_lambda_function" "optimized" {
memory_size = 1024 # Sweet spot for most functions
timeout = 30
}
Benchmark your functions to find the optimal memory setting.
3. Minimize Package Size
Smaller packages download faster:
# Multi-stage build for minimal Lambda images
FROM public.ecr.aws/lambda/provided:al2023 AS base
FROM base AS builder
RUN dnf install -y jq && dnf clean all
FROM base
COPY --from=builder /usr/bin/jq /usr/bin/jq
COPY bootstrap handler.sh /var/runtime/
RUN chmod +x /var/runtime/bootstrap
4. Use Lambda Layers Efficiently
Layers are cached across functions, reducing cold start time:
resource "aws_lambda_layer_version" "tools" {
filename = "tools-layer.zip"
layer_name = "common-tools"
source_code_hash = filebase64sha256("tools-layer.zip")
compatible_runtimes = ["provided.al2"]
}
Building Your Own Fast Custom Runtime
Step 1: Create the Bootstrap
#!/bin/bash
# bootstrap
set -euo pipefail
while true; do
HEADERS=$(mktemp)
EVENT=$(curl -sS -D "$HEADERS" \
"$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/next")
REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '\r' | cut -d: -f2 | tr -d ' ')
RESPONSE=$(timeout 30s ./handler.sh "$EVENT" 2>&1 || echo '{"error":"timeout"}')
curl -sS "$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/$REQUEST_ID/response" \
-d "$RESPONSE"
rm -f "$HEADERS"
done
Step 2: Create Your Handler
#!/bin/bash
# handler.sh
EVENT="$1"
NAME=$(echo "$EVENT" | jq -r '.name // "World"')
echo '{"message": "Hello, '"$NAME"'!", "timestamp": "'$(date -Iseconds)'"}'
Step 3: Build Optimized Container
FROM public.ecr.aws/lambda/provided:al2023
RUN yum update -y && yum install -y jq curl && yum clean all
COPY bootstrap handler.sh ./
RUN chmod +x bootstrap handler.sh
CMD ["handler.sh"]
Advanced Cold Start Optimization
1. Response Caching
Cache responses across invocations using /tmp (persisted between warm invocations):
# Cache API responses in /tmp for warm containers
CACHE_FILE="/tmp/api_health"
if [[ ! -f "$CACHE_FILE" ]] || [[ $(find "$CACHE_FILE" -mmin +5) ]]; then
curl -sS --connect-timeout 5 "https://api.example.com/health" > "$CACHE_FILE"
fi
cat "$CACHE_FILE"
2. Lazy Loading
Load resources only when needed:
load_config() {
if [[ -z "${CONFIG:-}" ]]; then
CONFIG=$(aws ssm get-parameter --name "/app/config" --query 'Parameter.Value' --output text)
fi
echo "$CONFIG"
}
3. Warm-up Strategies
Keep functions warm with scheduled invocations:
resource "aws_cloudwatch_event_rule" "warmup" {
name = "lambda-warmup"
description = "Keep Lambda functions warm"
schedule_expression = "rate(5 minutes)"
}
resource "aws_cloudwatch_event_target" "lambda" {
rule = aws_cloudwatch_event_rule.warmup.name
target_id = "WarmupTarget"
arn = aws_lambda_function.example.arn
input = jsonencode({"warmup": true})
}
Monitoring Cold Start Performance
CloudWatch Metrics
Track these key metrics:
- InitDuration - cold start time
- Duration - total execution time
- MemoryUtilized - actual memory usage
Custom Metrics
For occasional profiling (not every invocation — each put-metric-data call adds ~200ms):
# In your function
START_TIME=$(date +%s%3N)
# ... function logic ...
END_TIME=$(date +%s%3N)
DURATION=$((END_TIME - START_TIME))
aws cloudwatch put-metric-data \
--namespace "Lambda/Performance" \
--metric-data MetricName=ExecutionTime,Value=$DURATION,Unit=Milliseconds
Cost Impact of Cold Start Optimization
Before optimization (Node.js):
- Average duration: 1,240ms
- Memory: 152MB
- Monthly cost (1M requests): $3.34
After optimization (Custom runtime):
- Average duration: 316ms
- Memory: 36MB
- Monthly cost (1M requests): $0.39
Savings: 88% cost reduction
Calculated at $0.0000166667/GB-second + $0.20 per 1M requests (us-east-1, arm64 pricing).
When Cold Start Optimization Matters Most
Critical use cases:
- User-facing APIs - every millisecond counts
- Real-time processing - IoT, streaming data
- Synchronous workflows - API Gateway integrations
- Cost-sensitive applications - high-volume, low-margin
Less critical:
- Batch processing - cold start amortized over long runs
- Scheduled tasks - can use provisioned concurrency
- Internal tools - user tolerance for latency
Getting Started
Try our optimized custom runtime:
# Use our pre-built runtime
docker pull ghcr.io/ql4b/lambda-shell-runtime:tiny
# Or build your own
git clone https://github.com/ql4b/lambda-shell-runtime
cd lambda-shell-runtime
docker build -t my-fast-runtime .
Result: 90% faster cold starts, 75% lower costs, same functionality.
Lambda cold start optimization isn’t just about performance - it’s about cost efficiency and user experience. Custom runtimes give you the control to optimize for what matters most.