Introduction
Serverless is marketed as “pay-per-execution,” but many organizations experience bill shock after deploying to AWS Lambda and DynamoDB. The problem: hidden costs from function duration, request patterns, and database throughput that quickly add up.
This guide reveals common serverless cost traps and practical strategies to reduce bills by 30-70% without sacrificing performance.
The Serverless Cost Problem
Real-World Scenario
A startup deployed a Node.js API on Lambda with DynamoDB:
- Expected cost: $50/month
- Actual cost after 3 months: $8,400/month (168x over budget)
Root causes:
- Inefficient code causing 10-second execution times (average)
- Database queries within loops (N+1 problem)
- No monitoring or cost alerts
- Wrong DynamoDB billing mode
AWS Lambda Pricing Fundamentals
How Lambda Billing Works
Monthly Cost = (Requests × Request Charge) + (Duration × Duration Charge)
Pricing Breakdown
Request Charges:
- First 1 million requests/month: FREE
- Additional requests: $0.20 per million ($0.0000002/request)
Duration Charges:
- Allocated memory: 128 MB to 10,240 MB
- Duration: 1 millisecond to 15 minutes
- Cost: $0.0000166667 per GB-second
Cost Calculation Example
Function Configuration: 512 MB memory
Monthly Requests: 10 million
Average Duration: 3 seconds
GB-seconds = (Memory in GB) × (Duration in seconds) × (Number of executions)
= (512/1024) × 3 × 10,000,000
= 0.5 × 3 × 10,000,000
= 15,000,000 GB-seconds
Monthly Cost = (Duration cost) + (Request cost)
= (15,000,000 × $0.0000166667) + (10,000,000 × $0.0000002)
= $250 + $2
= $252/month
If duration was 10 seconds:
= (50,000,000 × $0.0000166667) + $2
= $833/month (3.3x more expensive!)
Key Insight
Duration is 125x more expensive than requests. Optimizing duration has massive cost impact.
Lambda Cost Trap #1: Inefficient Code
The Problem
Slow code = longer execution time = exponential costs
Example: Database Loop
# BAD: 1,000 database calls
def handler(event, context):
user_ids = event['user_ids'] # 1,000 items
results = []
for user_id in user_ids:
# Each call = 100ms
user = dynamodb.get_item(Key={'id': user_id})
results.append(user)
return results
# Execution time: 1,000 × 100ms = 100 seconds
# Cost per invocation: 0.5 GB × 100 sec × $0.0000166667 = $0.0833
# 1,000 invocations/day: $83.30/day = $2,500/month
The Solution: Batch Operations
# GOOD: Batch read
def handler(event, context):
user_ids = event['user_ids']
# Batch read (25 items max per DynamoDB request)
results = []
for i in range(0, len(user_ids), 25):
batch = user_ids[i:i+25]
response = dynamodb.batch_get_item(
RequestItems={
'Users': {
'Keys': [{'id': uid} for uid in batch]
}
}
)
results.extend(response['Responses']['Users'])
return results
# Execution time: 40 requests × 50ms = 2 seconds
# Cost per invocation: 0.5 GB × 2 sec × $0.0000166667 = $0.0000333
# 1,000 invocations/day: $0.033/day = $1/month
Savings: From $2,500/month to $1/month (2,500x reduction!)
Optimization Strategies
- Use batch operations (batch_get_item, batch_write_item)
- Implement request caching (reduce database queries)
- Choose faster languages (Go, Rust faster than Python, Node)
- Reduce memory bloat (trim dependencies)
- Use Lambda@Edge (CloudFront) for origin selection
Lambda Cost Trap #2: Memory Over-Allocation
CPU Scales with Memory
AWS Lambda allocates CPU proportionally to memory:
- 128 MB = 0.017 vCPU (slowest)
- 512 MB = 0.068 vCPU
- 1024 MB = 0.135 vCPU
- 3008 MB = 0.403 vCPU (fastest)
More memory = faster execution = potentially lower total cost.
Cost vs Speed Tradeoff
Example: Data processing function
Processing 1GB file:
512 MB allocation: 60 seconds execution → $0.0500 cost
1024 MB allocation: 30 seconds execution → $0.0500 cost
3008 MB allocation: 10 seconds execution → $0.0500 cost
Same cost! But 3008 MB risks:
- Over-paying for unused CPU
- Timeout issues with large memory overhead
The Optimization
Find the sweet spot:
- Test at 512 MB baseline
- Measure execution time
- Increase memory 256 MB at a time
- Stop when execution time doesn’t improve significantly
- Use Lambda Power Tuning to automate
DynamoDB Cost Traps
DynamoDB Pricing Models
On-Demand Pricing (Default)
- Requests: $1.25 per million write units
- Requests: $0.25 per million read units
- Storage: $0.25 per GB/month
- Good for: Variable, unpredictable workloads
Provisioned Capacity
- Read: $0.47 per read unit/hour (monthly: $342)
- Write: $2.37 per write unit/hour (monthly: $1,730)
- Good for: Predictable, steady-state workloads
Cost Trap #1: Wrong Billing Mode
Scenario: Running in on-demand mode with predictable traffic
On-Demand (10 million reads/month):
= 10,000,000 / 1,000,000 × $0.25
= $2.50/month (read-heavy query)
With 10 provisioned read capacity:
= 10 units × $0.47/hour × 730 hours
= $3,431/month
But wait! 10 units = 10 × 100,000 = 1,000,000 reads/month
This matches exactly!
If traffic fluctuates:
- Peak: needs 50 units = $17,155/month
- Off-peak: needs 5 units = $1,715/month
- Mixed workload: on-demand might be cheaper
Solution: Mode Selection Matrix
| Workload | Billing Mode | Reason |
|---|---|---|
| Predictable traffic | Provisioned | 70% cheaper for steady workloads |
| Variable traffic | On-Demand | Avoid over-paying for peaks |
| Development/Testing | On-Demand | Low usage, lower bills |
| Production microservices | Provisioned + auto-scaling | Consistent traffic, reserved capacity |
| Analytics/batch | On-Demand | Bursty, unpredictable |
Cost Trap #2: Inefficient Queries
Problem: Scanning instead of querying
# BAD: Scan (reads all items)
response = dynamodb.scan(
TableName='Orders',
FilterExpression='user_id = :uid',
ExpressionAttributeValues={':uid': '12345'}
)
# Scans 1,000,000 items, reads 1,000,000 RU
# Cost: $0.25
# GOOD: Query (uses index)
response = dynamodb.query(
TableName='Orders',
KeyConditionExpression='user_id = :uid',
ExpressionAttributeValues={':uid': '12345'}
)
# Queries only items for user, reads 50 RU
# Cost: $0.0000125
Cost reduction: 99.995% savings!
Cost Trap #3: Hot Partitions
Problem: All traffic goes to one partition key
Table: Users (partition key: user_id)
Peak traffic: 10,000 requests/second to same user_id
DynamoDB allocates throughput per partition
All traffic → one partition → throttling
Solution:
- Use sharding key: user_id + month
- Distribute traffic across partitions
- Reduces hot partition throttling and costs
DynamoDB Cost Reduction Strategies
Strategy 1: Use DynamoDB Streams + Lambda
Instead of polling database repeatedly:
# BAD: Lambda polls DynamoDB every minute
def handler(event, context):
# Runs 1,440 times/day = 1,440 queries
response = dynamodb.query(...)
# Cost: 1,440 × 4 RU = 5,760 RU/day
# GOOD: DynamoDB Streams trigger Lambda
# Lambda only runs when data actually changes
# Runs ~50 times/day = 50 × 4 RU = 200 RU/day
# Savings: 96%
Strategy 2: TTL (Time-to-Live) for Auto-Deletion
# Items automatically deleted after expiration
# Saves storage costs and query time
item = {
'id': '12345',
'data': {...},
'ttl': int(time.time()) + (30 * 24 * 60 * 60) # 30 days
}
dynamodb.put_item(TableName='Items', Item=item)
# Item auto-deleted after TTL expiration
# No manual cleanup needed
# Storage cost reduced
Strategy 3: Global Secondary Indexes (GSI) Optimization
# Only create GSI if you actually use it
# Each GSI duplicates data and uses throughput
# Too many GSI = multiplied costs
# Better: Denormalize data into main table
# Trade: Larger item size vs faster queries
# Result: Fewer indexes, lower cost
Strategy 4: DynamoDB DAX Caching
Scenario: Frequent reads of same data
Without DAX:
- 1,000 read requests/minute
- 1,000 RU/minute = $432/month
With DAX cache:
- 900 cache hits, 100 misses
- 100 RU/minute = $43.20/month
- Savings: 90%
- Cost: DAX cluster (~$200/month)
Net savings: $189/month + better latency
Monitoring and Alerting
Set Up Cost Alerts
-
AWS Budgets
- Set monthly limit
- Alert when approaching threshold
-
CloudWatch Alarms
- Monitor Lambda duration
- Monitor DynamoDB consumed capacity
-
Cost Explorer
- Daily cost dashboard
- Identify cost spikes
Example Alarm Configuration
MonitorDuration:
Type: AWS::CloudWatch::Alarm
Properties:
MetricName: Duration
Namespace: AWS/Lambda
Statistic: Average
Period: 300
EvaluationPeriods: 2
Threshold: 5000 # milliseconds
AlarmActions:
- arn:aws:sns:us-east-1:123456789:alerts
Real-World Case Studies
Case Study 1: SaaS API Platform
Before:
- Lambda: 500ms average
- DynamoDB: On-demand mode
- Monthly bill: $8,400
Changes:
- Optimized code: 100ms average (5x faster)
- Switched to provisioned capacity
- Implemented caching layer
- Added indexes for common queries
After:
- Lambda cost: $200/month
- DynamoDB cost: $800/month
- Total: $1,000/month
Savings: $7,400/month (88% reduction)
Case Study 2: Event Processing Pipeline
Before:
- Processing 10 million events/month
- Average Lambda duration: 8 seconds
- Monthly cost: $4,200
Changes:
- Batch processing instead of per-event
- Average duration: 0.5 seconds
- Used provisioned capacity for DynamoDB
After:
- Lambda cost: $250/month
- DynamoDB cost: $400/month
- Total: $650/month
Savings: $3,550/month (85% reduction)
Optimization Checklist
- Profile Lambda function duration
- Remove unnecessary dependencies
- Implement batch operations
- Use local caching
- Analyze DynamoDB query patterns
- Remove unused Global Secondary Indexes
- Switch to provisioned capacity if predictable
- Implement DynamoDB Streams
- Add TTL to transient data
- Set up cost monitoring
- Enable X-Ray for bottleneck identification
Glossary
- GB-Second: 1 GB of memory used for 1 second
- Read Unit (RU): 4 KB data read (eventually consistent)
- Write Unit (WU): 1 KB data written
- On-Demand: Pay per request, variable cost
- Provisioned: Reserved capacity, fixed cost
- Scan: Read all items in table
- Query: Read items by partition key
- GSI: Global Secondary Index for alternative access patterns
- TTL: Time-to-Live, auto-expire items
- DAX: DynamoDB Accelerator, in-memory cache
Resources
- AWS Lambda Pricing
- DynamoDB Pricing
- Lambda Power Tuning
- DynamoDB Best Practices
- AWS Cost Optimization
Related Articles
- AWS Cost Optimization: Reserved Instances vs Savings Plans
- Container Cost Analysis: Docker, Kubernetes Economics
- Data Transfer Costs: How to Save $100k+/year
- Finops Automation: CloudHealth, Cloudability, Kubecost
Comments