Monday Cloud Tip: AWS Lambda Performance Optimization That Actually Works

Your weekly dose of actionable cloud wisdom to start the week right

The Problem

Your Lambda function works perfectly in testing, but in production it’s a performance nightmare. Users wait 3-5 seconds for responses because of cold starts, your costs are spiralling out of control, and you’re questioning whether serverless was the right choice. Meanwhile, other teams have blazing-fast Lambda functions that cost a fraction of yours.

The Solution

Optimise Lambda performance with proven techniques that balance speed and cost. Most performance issues stem from poor memory allocation, cold start problems, and inefficient code patterns that are easily fixable once you know what to look for.

Essential Performance Optimizations:

1. Right-Size Your Memory Allocation

# Use AWS Lambda Power Tuning to find the sweet spot
# Rule of thumb: More memory = faster CPU + potentially lower cost

# Before: 128MB (slowest, often more expensive)
# After: 1024MB (10x faster, often cheaper per request)

import json
import time

def lambda_handler(event, context):
    # Heavy computation example
    start_time = time.time()
    
    # Your business logic here
    result = complex_calculation(event['data'])
    
    execution_time = time.time() - start_time
    
    return {
        'statusCode': 200,
        'body': json.dumps({
            'result': result,
            'execution_time_ms': round(execution_time * 1000, 2)
        })
    }

2. Reduce Cold Start Impact

import boto3
import json

# Move expensive operations outside handler (runs only on cold start)
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('my-table')

# Reuse database connections
session = boto3.Session()
s3_client = session.client('s3')

def lambda_handler(event, context):
    # This runs on every invocation (keep it light)
    try:
        # Fast database operation using pre-initialized connection
        response = table.get_item(Key={'id': event['id']})
        
        return {
            'statusCode': 200,
            'body': json.dumps(response['Item'])
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }

3. Optimise Package Size

# Use Lambda Layers for shared dependencies
# Layer creation example
mkdir python
pip install requests -t python/
zip -r requests-layer.zip python/

# Create layer
aws lambda publish-layer-version \
    --layer-name requests-layer \
    --zip-file fileb://requests-layer.zip \
    --compatible-runtimes python3.9

# In your function, exclude heavy packages from deployment
echo "requests" > requirements-layer.txt
pip install -r requirements.txt -t . --exclude-files requirements-layer.txt

4. Enable Provisioned Concurrency for Critical Functions

# For functions that can't tolerate cold starts
aws lambda put-provisioned-concurrency-config \
    --function-name my-critical-function \
    --qualifier $LATEST \
    --provisioned-concurrency-count 10

# Use with auto-scaling for cost efficiency
aws application-autoscaling register-scalable-target \
    --service-namespace lambda \
    --scalable-dimension lambda:function:ProvisionedConcurrency \
    --resource-id function:my-critical-function:$LATEST \
    --min-capacity 5 \
    --max-capacity 50

5. Connection Pooling and Caching

import pymysql
import json
from functools import lru_cache

# Database connection reuse
connection = None

def get_db_connection():
    global connection
    if connection is None or not connection.open:
        connection = pymysql.connect(
            host='your-rds-endpoint',
            user='username',
            password='password',
            database='your-db',
            connect_timeout=5
        )
    return connection

# In-memory caching for expensive operations
@lru_cache(maxsize=128)
def expensive_calculation(input_data):
    # Cached results persist for the lifetime of the container
    return complex_operation(input_data)

def lambda_handler(event, context):
    # Reuse connection and cached results
    db = get_db_connection()
    result = expensive_calculation(event['data'])
    
    return {
        'statusCode': 200,
        'body': json.dumps({'result': result})
    }

Performance Monitoring Setup

# Add performance monitoring to your functions
import time
import os

def performance_monitor(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        start_memory = get_memory_usage()
        
        result = func(*args, **kwargs)
        
        end_time = time.time()
        end_memory = get_memory_usage()
        
        # Log performance metrics
        print(f"Execution time: {(end_time - start_time) * 1000:.2f}ms")
        print(f"Memory used: {end_memory - start_memory}MB")
        print(f"Cold start: {os.environ.get('AWS_LAMBDA_INITIALIZATION_TYPE', 'provisioned-concurrency')}")
        
        return result
    return wrapper

@performance_monitor
def lambda_handler(event, context):
    # Your function logic here
    pass

Why It Matters

  • User Experience: Sub-second response times keep users happy
  • Cost Efficiency: Optimised functions often cost 50-80% less
  • Scalability: Better performance = more concurrent users supported
  • Reliability: Faster functions have lower timeout risk

Try This Week

  1. Measure current performance – Add timing logs to your worst-performing function
  2. Run AWS Lambda Power Tuning – Find the optimal memory allocation
  3. Move initialization outside handler – Reduce cold start impact
  4. Enable X-Ray tracing – Identify performance bottlenecks visually

Quick Win: Memory Optimization Script

# Test different memory settings programmatically
import boto3
import json

def test_memory_configurations(function_name, test_payload):
    lambda_client = boto3.client('lambda')
    memory_configs = [128, 256, 512, 1024, 1536, 2048, 3008]
    results = []
    
    for memory in memory_configs:
        # Update function memory
        lambda_client.update_function_configuration(
            FunctionName=function_name,
            MemorySize=memory
        )
        
        # Wait for update to complete
        lambda_client.get_waiter('function_updated').wait(FunctionName=function_name)
        
        # Test performance
        response = lambda_client.invoke(
            FunctionName=function_name,
            Payload=json.dumps(test_payload)
        )
        
        # Parse results
        duration = response['ResponseMetadata']['HTTPHeaders'].get('x-amzn-trace-id')
        billing_duration = response.get('LogResult', '')
        
        results.append({
            'memory': memory,
            'duration': duration,
            'cost_per_gb_second': memory * 0.0000166667  # Current pricing
        })
    
    return results

Common Performance Killers

  • Undersized memory: 128MB is rarely optimal for real workloads
  • Large deployment packages: Keep under 10MB for faster cold starts
  • Database connection per request: Reuse connections aggressively
  • Synchronous external calls: Use async where possible
  • No monitoring: You can’t optimize what you don’t measure

Advanced Tips

  • Use ARM Graviton2 processors: 20% better price-performance for compatible workloads
  • Implement circuit breakers: Fail fast when dependencies are slow
  • Optimize VPC configuration: VPC cold starts add 10+ seconds
  • Consider Step Functions: Break up long-running processes

Pro Tip: AWS Lambda Power Tuning is a free, open-source tool that automatically finds the optimal memory configuration for your function. It can save you hours of manual testing and often reduces costs by 50%+.