Skip to main content
โšก Calmops

How to Optimize Rust Binary Size for Lower Lambda Execution Costs

Introduction

AWS Lambda charges based on execution time and memory allocation. For Rust developers, binary size directly impacts three critical metrics: cold start time, deployment package size, and execution cost. A typical unoptimized Rust Lambda function can be 50-100MB, but with proper optimization techniques, you can reduce it to 2-5MBโ€”cutting costs by 50-70% and improving cold start times by 60-80%.

This comprehensive guide covers every optimization technique available, from compiler flags to runtime strategies, with real-world benchmarks and cost calculations.

Core Concepts and Terminology

Binary Size: The total size of the compiled executable file, measured in bytes.

Cold Start: The time required to initialize a Lambda function on its first invocation or after a period of inactivity.

Warm Start: The time to execute a Lambda function when the container is already initialized.

Execution Cost: AWS charges $0.0000166667 per GB-second of execution time.

Memory Cost: Lambda pricing is based on allocated memory (128MB to 10,240MB).

Deployment Package: The ZIP file containing your Lambda function code and dependencies.

Link-Time Optimization (LTO): Compiler optimization that occurs during the linking phase.

MUSL: A lightweight C standard library used for creating smaller, more portable binaries.

UPX: Ultimate Packer for eXecutables, a tool that compresses executable files.

Codegen Units: The number of parallel code generation units during compilation.

Strip: The process of removing debugging symbols from compiled binaries.

The Lambda Cost Challenge

Typical Rust Lambda Cost Breakdown
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Unoptimized Rust Lambda (50MB binary)                   โ”‚
โ”‚                                                         โ”‚
โ”‚ Deployment Costs:                                       โ”‚
โ”‚ โ”œโ”€ Package size: 50MB (slow deployment)                โ”‚
โ”‚ โ”œโ”€ Cold start: 2000-3000ms                             โ”‚
โ”‚ โ””โ”€ Warm start: 100-200ms                               โ”‚
โ”‚                                                         โ”‚
โ”‚ Execution Costs (1M invocations/month):                 โ”‚
โ”‚ โ”œโ”€ Cold starts (10%): 100K ร— 2500ms = 250K seconds    โ”‚
โ”‚ โ”œโ”€ Warm starts (90%): 900K ร— 150ms = 135K seconds     โ”‚
โ”‚ โ”œโ”€ Total: 385K seconds ร— $0.0000166667 = $6.42        โ”‚
โ”‚ โ””โ”€ Monthly cost: $6.42 ร— 12 = $77/year                โ”‚
โ”‚                                                         โ”‚
โ”‚ Optimization Opportunity: 60-70% reduction             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Optimized Rust Lambda (3MB binary)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Optimized Rust Lambda (3MB binary)                      โ”‚
โ”‚                                                         โ”‚
โ”‚ Deployment Costs:                                       โ”‚
โ”‚ โ”œโ”€ Package size: 3MB (fast deployment)                 โ”‚
โ”‚ โ”œโ”€ Cold start: 600-800ms                               โ”‚
โ”‚ โ””โ”€ Warm start: 100-200ms                               โ”‚
โ”‚                                                         โ”‚
โ”‚ Execution Costs (1M invocations/month):                 โ”‚
โ”‚ โ”œโ”€ Cold starts (10%): 100K ร— 700ms = 70K seconds      โ”‚
โ”‚ โ”œโ”€ Warm starts (90%): 900K ร— 150ms = 135K seconds     โ”‚
โ”‚ โ”œโ”€ Total: 205K seconds ร— $0.0000166667 = $3.42        โ”‚
โ”‚ โ””โ”€ Monthly cost: $3.42 ร— 12 = $41/year                โ”‚
โ”‚                                                         โ”‚
โ”‚ Annual Savings: $36 per Lambda function                โ”‚
โ”‚ For 100 functions: $3,600/year                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Optimization Techniques

Technique 1: Compiler Optimization Flags (25-35% Reduction)

The most impactful optimization is configuring your Cargo.toml with aggressive compiler flags:

# Cargo.toml - Optimized for Lambda
[package]
name = "lambda-function"
version = "0.1.0"
edition = "2021"

[dependencies]
lambda_runtime = "0.8"
serde_json = "1.0"
tokio = { version = "1", features = ["rt-core", "macros"] }

[profile.release]
# Optimize for size instead of speed
opt-level = "z"

# Enable Link-Time Optimization
lto = true

# Use single codegen unit for better optimization
codegen-units = 1

# Strip all symbols from binary
strip = true

# Enable panic abort instead of unwinding
panic = "abort"

# Reduce debug info
debug = false

# Optimize for size in dependencies too
[profile.release.package."*"]
opt-level = "z"
strip = true

Expected Results:

  • Default release build: 50MB
  • With compiler flags: 32-38MB (25-35% reduction)

Technique 2: Dependency Minimization (20-30% Reduction)

Most Rust projects include unnecessary features in their dependencies:

# BEFORE: Full-featured dependencies (50MB)
[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.11", features = ["json", "cookies", "blocking"] }
tracing = { version = "0.1", features = ["full"] }

# AFTER: Minimal features (35-40MB)
[dependencies]
# Only include features you actually use
tokio = { version = "1", features = ["rt-core", "macros", "time"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.11", features = ["json"] }
tracing = { version = "0.1", features = ["core"] }

# Remove unused dependencies entirely
# Before: 15 dependencies
# After: 8 dependencies

Dependency Analysis:

# Analyze binary size by dependency
cargo bloat --release

# Output example:
# File  .text     Size      Crate Name
# 1.2%   1.2%   1.2MiB tokio <tokio::runtime::Runtime as core::default::Default>::default
# 0.8%   0.8%   0.8MiB serde_json serde_json::de::from_slice
# 0.6%   0.6%   0.6MiB reqwest reqwest::Client::new

Expected Results:

  • With full features: 50MB
  • With minimal features: 35-40MB (20-30% reduction)

Technique 3: MUSL Target (15-20% Reduction)

Using the MUSL target instead of glibc produces smaller binaries:

# Install MUSL target
rustup target add x86_64-unknown-linux-musl

# Build with MUSL
cargo build --release --target x86_64-unknown-linux-musl

# Result: 28-32MB (vs. 35-40MB with glibc)

Why MUSL is Smaller:

  • MUSL is a lightweight C standard library
  • Statically linked (no external dependencies)
  • Optimized for embedded systems
  • Perfect for Lambda’s minimal environment

Expected Results:

  • glibc target: 35-40MB
  • MUSL target: 28-32MB (15-20% reduction)

Technique 4: Symbol Stripping (10-15% Reduction)

Remove debugging symbols from the compiled binary:

# Automatic stripping via Cargo.toml (recommended)
# Already configured in [profile.release] above

# Manual stripping (if needed)
strip target/x86_64-unknown-linux-musl/release/bootstrap

# Verify symbols were removed
file target/x86_64-unknown-linux-musl/release/bootstrap
# Output: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), 
#         dynamically linked, stripped

Expected Results:

  • Before stripping: 32MB
  • After stripping: 27-29MB (10-15% reduction)

Technique 5: UPX Compression (40-60% Reduction)

UPX compresses executable files, reducing size significantly:

# Install UPX
# macOS
brew install upx

# Linux
sudo apt-get install upx

# Windows
# Download from https://upx.github.io/

# Compress binary with best compression
upx --best --lzma target/x86_64-unknown-linux-musl/release/bootstrap -o bootstrap

# Verify compression
ls -lh target/x86_64-unknown-linux-musl/release/bootstrap
ls -lh bootstrap

# Example output:
# Before: 27M bootstrap
# After:  8M bootstrap (70% reduction!)

UPX Compression Levels:

# Fast compression (less effective)
upx -1 bootstrap

# Balanced compression
upx -9 bootstrap

# Best compression (slower decompression)
upx --best --lzma bootstrap

# Results:
# -1: 15MB (45% reduction)
# -9: 10MB (63% reduction)
# --best --lzma: 8MB (70% reduction)

Important Note: UPX adds decompression overhead (~100-200ms to cold start), but the smaller package size usually compensates.

Expected Results:

  • Before UPX: 27-29MB
  • After UPX: 8-10MB (65-70% reduction)

Technique 6: Runtime Optimization (5-10% Reduction)

Optimize your Rust code for Lambda:

// Optimized Lambda handler
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Error> {
    // Initialize once at startup
    let client = initialize_client();
    
    run(service_fn(|event| {
        function_handler(event, &client)
    }))
    .await
}

async fn function_handler(
    event: LambdaEvent<Value>,
    client: &Client,
) -> Result<Value, Error> {
    // Reuse client across invocations
    let result = client.process(&event.payload).await?;
    
    Ok(json!({
        "statusCode": 200,
        "body": result
    }))
}

fn initialize_client() -> Client {
    // Initialize expensive resources once
    Client::new()
}

Expected Results:

  • Optimized code: 5-10% reduction in execution time
  • Reused connections: 50-70% faster warm starts

Complete Optimization Workflow

#!/bin/bash
set -e

echo "Building optimized Rust Lambda..."

# Step 1: Clean previous builds
cargo clean

# Step 2: Build with MUSL target
echo "Building with MUSL target..."
cargo build --release --target x86_64-unknown-linux-musl

# Step 3: Verify binary size
echo "Binary size before optimization:"
ls -lh target/x86_64-unknown-linux-musl/release/bootstrap

# Step 4: Compress with UPX
echo "Compressing with UPX..."
upx --best --lzma target/x86_64-unknown-linux-musl/release/bootstrap -o bootstrap

# Step 5: Verify final size
echo "Binary size after optimization:"
ls -lh bootstrap

# Step 6: Create deployment package
echo "Creating deployment package..."
zip lambda.zip bootstrap

# Step 7: Verify package size
echo "Deployment package size:"
ls -lh lambda.zip

# Step 8: Upload to Lambda
echo "Uploading to Lambda..."
aws lambda update-function-code \
    --function-name my-rust-function \
    --zip-file fileb://lambda.zip

Binary Size Comparison

Optimization Size Cold Start Reduction
Default build 50MB 2500ms โ€”
Release flags 38MB 2000ms 24%
Minimal deps 35MB 1900ms 30%
MUSL target 30MB 1800ms 40%
Stripped 27MB 1700ms 46%
UPX compressed 8MB 1900ms 84%
All combined 8MB 1900ms 84%

Cost Impact Analysis

Scenario 1: Small Function (100K invocations/month)

Unoptimized (50MB, 2500ms cold start):
- Cold starts (10%): 10K ร— 2500ms = 25K seconds
- Warm starts (90%): 90K ร— 150ms = 13.5K seconds
- Total: 38.5K seconds ร— $0.0000166667 = $0.64/month
- Annual: $7.68

Optimized (8MB, 1900ms cold start):
- Cold starts (10%): 10K ร— 1900ms = 19K seconds
- Warm starts (90%): 90K ร— 150ms = 13.5K seconds
- Total: 32.5K seconds ร— $0.0000166667 = $0.54/month
- Annual: $6.48

Annual Savings: $1.20 per function
For 100 functions: $120/year

Scenario 2: Medium Function (1M invocations/month)

Unoptimized (50MB, 2500ms cold start):
- Cold starts (10%): 100K ร— 2500ms = 250K seconds
- Warm starts (90%): 900K ร— 150ms = 135K seconds
- Total: 385K seconds ร— $0.0000166667 = $6.42/month
- Annual: $77.04

Optimized (8MB, 1900ms cold start):
- Cold starts (10%): 100K ร— 1900ms = 190K seconds
- Warm starts (90%): 900K ร— 150ms = 135K seconds
- Total: 325K seconds ร— $0.0000166667 = $5.42/month
- Annual: $65.04

Annual Savings: $12 per function
For 100 functions: $1,200/year

Scenario 3: High-Traffic Function (10M invocations/month)

Unoptimized (50MB, 2500ms cold start):
- Cold starts (10%): 1M ร— 2500ms = 2.5M seconds
- Warm starts (90%): 9M ร— 150ms = 1.35M seconds
- Total: 3.85M seconds ร— $0.0000166667 = $64.17/month
- Annual: $770.04

Optimized (8MB, 1900ms cold start):
- Cold starts (10%): 1M ร— 1900ms = 1.9M seconds
- Warm starts (90%): 9M ร— 150ms = 1.35M seconds
- Total: 3.25M seconds ร— $0.0000166667 = $54.17/month
- Annual: $650.04

Annual Savings: $120 per function
For 100 functions: $12,000/year

Best Practices

1. Profile Before Optimizing

# Analyze what's taking up space
cargo bloat --release --target x86_64-unknown-linux-musl

# Identify largest dependencies
cargo tree --depth 1

2. Test Cold Start Times

# Measure cold start time
aws lambda invoke \
    --function-name my-rust-function \
    --payload '{}' \
    response.json

# Check CloudWatch logs for duration
aws logs tail /aws/lambda/my-rust-function --follow

3. Monitor Binary Size in CI/CD

# GitHub Actions example
- name: Check binary size
  run: |
    SIZE=$(stat -f%z target/x86_64-unknown-linux-musl/release/bootstrap)
    if [ $SIZE -gt 10485760 ]; then  # 10MB
      echo "Binary size too large: $SIZE bytes"
      exit 1
    fi

4. Use Feature Flags for Optional Dependencies

[dependencies]
tokio = { version = "1", features = ["rt-core", "macros"] }
serde = { version = "1", features = ["derive"] }

[features]
default = []
full = ["tokio/full", "serde/full"]

5. Consider Alternatives to Heavy Dependencies

// Instead of serde_json for simple cases
use std::collections::HashMap;

// Instead of tokio for simple async
use std::future::Future;

// Instead of reqwest for simple HTTP
use std::net::TcpStream;

Common Pitfalls

Pitfall 1: UPX Decompression Overhead

Problem: UPX adds 100-200ms to cold start time.

Solution: Measure actual cold start time with UPX. Usually the smaller package size compensates.

Pitfall 2: Removing Necessary Features

Problem: Removing features breaks functionality.

Solution: Test thoroughly. Use feature flags to keep optional features.

Pitfall 3: Ignoring Warm Start Performance

Problem: Optimizing only for size, not runtime performance.

Solution: Balance size optimization with runtime performance. Measure both.

Pitfall 4: Not Updating Dependencies

Problem: Old dependencies can be larger.

Solution: Keep dependencies updated. Newer versions often have better optimization.

Resources and Further Learning

Official Documentation

Tools

Learning Resources

Conclusion

Optimizing Rust binary size for Lambda is essential for cost-effective serverless applications. By combining compiler optimizations, dependency minimization, MUSL targeting, and UPX compression, you can reduce binary size by 80-85% and cold start times by 60-70%.

Key Takeaways:

  • Compiler flags: 25-35% reduction
  • Minimal dependencies: 20-30% reduction
  • MUSL target: 15-20% reduction
  • Symbol stripping: 10-15% reduction
  • UPX compression: 65-70% reduction
  • Total potential: 80-85% reduction

Implementation Priority:

  1. Start with compiler flags (easiest, high impact)
  2. Minimize dependencies (medium effort, high impact)
  3. Use MUSL target (easy, good impact)
  4. Add UPX compression (easy, very high impact)
  5. Optimize runtime code (ongoing)

Expected ROI: For 100 Lambda functions with 1M invocations each, optimization can save $1,200-$12,000 annually while improving user experience through faster cold starts.

Comments