Benchmarking Lambda cold start performance: Container images vs ZIP packages
This repository contains the benchmark code and results for the blog post "When Container Images Beat ZIP Packages: Lambda Cold Start Surprises".
This benchmark compares two Lambda packaging approaches using identical 5MB Go bootstrap code:
- ZIP Package: Go bootstrap + shell handler deployed as ZIP with
provided.al2023runtime - Container Image: Same bootstrap and handler packaged as container image
Container images consistently outperformed ZIP packages:
| Metric | ZIP Package | Container Image | Improvement |
|---|---|---|---|
| Average Init | 42.61ms | 33.51ms | 21% faster |
| P80 | 47.30ms | 36.46ms | 23% faster |
- Go binary handles Lambda Runtime API communication (eliminates
curloverhead) - Bash functions handle business logic (simple scripting)
// Fast HTTP client for Lambda API
func (c *runtimeAPIClient) getNextInvocation() (string, []byte, error) {
resp, err := c.httpClient.Get(c.baseURL + "next")
// ... handle response
}
// Execute shell function
func executeShellHandler(handlerFile, handlerFunc string, eventData []byte) ([]byte, error) {
shellCmd := fmt.Sprintf("source %s && %s", handlerFile, handlerFunc)
cmd := exec.Command("bash", "-c", shellCmd)
return cmd.Output()
}provided-dev/
├── app/src/
│ ├── bootstrap # Compiled Go runtime (5MB)
│ └── handler.sh # Shell handler functions
├── infra/
│ └── main.tf # Terraform for both ZIP and container deployments
├── benchmark # Benchmark runner script
├── invoke # Lambda invocation script
└── data/ # Benchmark results
- AWS CLI configured
- Terraform
- Go 1.21+
- Docker
- Build the Go bootstrap:
cd app/src/hybrid
go build -ldflags="-w -s" -o ../bootstrap main.go- Deploy infrastructure:
cd infra
terraform init
terraform apply- Run benchmarks:
# Benchmark ZIP package
./benchmark cloudless-lambda-os-only-lab-function
# Benchmark container image
./benchmark cloudless-lambda-os-only-lab-imageThe benchmark uses a clever approach to ensure all invocations are cold starts:
- Handler sleep: Each handler includes
sleep 5to keep containers busy - High concurrency: 60 parallel invocations force Lambda to create new containers
- No artificial updates: Avoids function configuration changes that might interfere with caching
# Benchmark runner
seq 1 120 | parallel -j 60 "$INVOKE_SCRIPT"# Extract init durations from CloudWatch logs
aws logs tail --since 1h "/aws/lambda/your-function" \
| grep "REPORT" \
| grep -o -E 'Init Duration: (.+) ms' \
| cut -d' ' -f 3{
"init_count": 60,
"init_total_ms": 2557.03,
"init_average_ms": 42.61,
"p20_ms": 40.41,
"p40_ms": 40.92,
"p60_ms": 41.16,
"p80_ms": 47.30
}{
"init_count": 60,
"init_total_ms": 2010.67,
"init_average_ms": 33.51,
"p20_ms": 25.71,
"p40_ms": 28.21,
"p60_ms": 31.88,
"p80_ms": 36.46
}- S3 download: 5MB bootstrap downloaded during cold start
- File extraction: Unzip and filesystem setup
- Permission configuration: Runtime permission setup
- Pre-built layers: Bootstrap already in optimized layers
- No runtime I/O: No download/extraction during cold start
- Layer caching: Efficient container infrastructure caching
This benchmark suggests there's a crossover point where container images become more efficient:
- Small functions (< 1MB): ZIP extraction overhead is negligible
- Large runtimes (> 5MB): Container layer caching outperforms ZIP extraction
The infrastructure uses dynamic ECR image resolution:
data "aws_ecr_image" "runtime_image" {
repository_name = module.runtime.image.repository_name
image_tag = "latest"
}
module "lambda_image" {
source = "git::https://github.com/ql4b/terraform-aws-lambda-function.git?ref=v1.0.1"
package_type = "Image"
image_uri = "${module.runtime.image.name}@${data.aws_ecr_image.runtime_image.image_digest}"
# ...
}- Modify the handler to include appropriate sleep duration
- Adjust concurrency based on your function's expected load
- Collect sufficient samples (60+ cold starts recommended)
- Account for log propagation delay (10-30 seconds)
- Container images can be faster than ZIP packages for larger runtimes
- Conventional wisdom doesn't always apply at scale
- Real-world benchmarking reveals performance characteristics not captured in documentation
- Packaging format choice should be based on actual measurements, not assumptions
- lambda-shell-runtime - Custom Lambda runtime for Bash functions
MIT License - see LICENSE file for details.
This benchmark was created to validate the findings in "When Container Images Beat ZIP Packages: Lambda Cold Start Surprises". Results may vary based on function size, complexity, and AWS infrastructure changes.