Capacity planning: optimize costs and performance¶
The practice¶
Understand your access patterns and plan capacity accordingly. Choose the right capacity mode (on-demand vs provisioned) and monitor usage to optimize costs.
Proper capacity planning ensures your application has the throughput it needs while minimizing costs. DynamoDB offers two capacity modes, each suited for different workload patterns.
Why it matters¶
Cost optimization¶
- Provisioned capacity can be 5-10x cheaper than on-demand for steady workloads
- On-demand eliminates waste for unpredictable traffic
- Right-sizing prevents over-provisioning
Performance¶
- Adequate capacity prevents throttling
- Auto-scaling responds to traffic changes
- Burst capacity handles temporary spikes
Predictability¶
- Understand your costs before they occur
- Plan for growth and scaling
- Avoid surprise bills
Capacity modes comparison¶
| Aspect | On-demand | Provisioned |
|---|---|---|
| Best for | Unpredictable workloads | Steady, predictable traffic |
| Pricing | Per-request | Per-hour capacity |
| Cost | Higher per request | Lower for consistent traffic |
| Scaling | Automatic, instant | Manual or auto-scaling |
| Planning | None required | Requires capacity planning |
| Throttling risk | Very low | Possible if under-provisioned |
Choosing a Capacity Mode
Use on-demand for new applications, unpredictable workloads, or spiky traffic. Use provisioned for mature applications with steady, predictable traffic patterns to save costs.
Understanding capacity units¶
Read capacity units (RCU)¶
- 1 RCU = 1 strongly consistent read per second for items up to 4KB
- 1 RCU = 2 eventually consistent reads per second for items up to 4KB
- Items larger than 4KB consume additional RCU (rounded up)
// Examples of RCU consumption
// 1KB item, strongly consistent
// 1 RCU per read
// 1KB item, eventually consistent
// 0.5 RCU per read
// 10KB item, strongly consistent
// 3 RCU per read (10KB / 4KB = 2.5, rounded up to 3)
// 10KB item, eventually consistent
// 1.5 RCU per read
Write capacity units (WCU)¶
- 1 WCU = 1 write per second for items up to 1KB
- Items larger than 1KB consume additional WCU (rounded up)
// Examples of WCU consumption
// 500 bytes item
// 1 WCU per write
// 1KB item
// 1 WCU per write
// 3.5KB item
// 4 WCU per write (3.5KB / 1KB = 3.5, rounded up to 4)
Calculating capacity requirements¶
Step 1: understand your access patterns¶
// Track your operations
import { StatsCollector } from '@ddb-lib/stats'
const stats = new StatsCollector()
const table = new TableClient({
tableName: 'Users',
statsCollector: stats
})
// After running your application
const summary = stats.getSummary()
console.log('Operations per second:')
console.log(` Reads: ${summary.operations.get + summary.operations.query}`)
console.log(` Writes: ${summary.operations.put + summary.operations.update}`)
console.log(`Average item size: ${summary.avgItemSize} bytes`)
Step 2: calculate required capacity¶
// Calculate RCU requirements
function calculateRCU(
readsPerSecond: number,
avgItemSizeKB: number,
stronglyConsistent = true
): number {
const rcuPerRead = Math.ceil(avgItemSizeKB / 4)
const multiplier = stronglyConsistent ? 1 : 0.5
return Math.ceil(readsPerSecond * rcuPerRead * multiplier)
}
// Calculate WCU requirements
function calculateWCU(
writesPerSecond: number,
avgItemSizeKB: number
): number {
const wcuPerWrite = Math.ceil(avgItemSizeKB / 1)
return Math.ceil(writesPerSecond * wcuPerWrite)
}
// Example calculation
const readsPerSecond = 100
const writesPerSecond = 20
const avgItemSizeKB = 2
const requiredRCU = calculateRCU(readsPerSecond, avgItemSizeKB, true)
const requiredWCU = calculateWCU(writesPerSecond, avgItemSizeKB)
console.log(`Required capacity:`)
console.log(` RCU: ${requiredRCU}`)
console.log(` WCU: ${requiredWCU}`)
Step 3: add buffer for bursts¶
// Add 20-30% buffer for traffic spikes
const bufferMultiplier = 1.25
const provisionedRCU = Math.ceil(requiredRCU * bufferMultiplier)
const provisionedWCU = Math.ceil(requiredWCU * bufferMultiplier)
console.log(`Provisioned capacity (with 25% buffer):`)
console.log(` RCU: ${provisionedRCU}`)
console.log(` WCU: ${provisionedWCU}`)
Cost comparison¶
On-demand pricing¶
// On-demand pricing (approximate, varies by region)
const onDemandReadCost = 0.25 / 1_000_000 // $0.25 per million reads
const onDemandWriteCost = 1.25 / 1_000_000 // $1.25 per million writes
function calculateOnDemandCost(
readsPerMonth: number,
writesPerMonth: number
): number {
const readCost = readsPerMonth * onDemandReadCost
const writeCost = writesPerMonth * onDemandWriteCost
return readCost + writeCost
}
// Example: 100M reads, 20M writes per month
const monthlyCost = calculateOnDemandCost(100_000_000, 20_000_000)
console.log(`On-demand monthly cost: $${monthlyCost.toFixed(2)}`)
// Output: On-demand monthly cost: $50.00
Provisioned pricing¶
// Provisioned pricing (approximate, varies by region)
const provisionedRCUCost = 0.00013 / 3600 // $0.00013 per RCU-hour
const provisionedWCUCost = 0.00065 / 3600 // $0.00065 per WCU-hour
function calculateProvisionedCost(
rcu: number,
wcu: number,
hoursPerMonth = 730
): number {
const readCost = rcu * provisionedRCUCost * hoursPerMonth
const writeCost = wcu * provisionedWCUCost * hoursPerMonth
return readCost + writeCost
}
// Example: 50 RCU, 10 WCU
const monthlyCost = calculateProvisionedCost(50, 10)
console.log(`Provisioned monthly cost: $${monthlyCost.toFixed(2)}`)
// Output: Provisioned monthly cost: $1.31
Cost comparison example¶
// Scenario: Steady traffic
// 100 reads/sec, 20 writes/sec, 2KB items
// 259M reads, 52M writes per month
const onDemandCost = calculateOnDemandCost(259_000_000, 52_000_000)
const provisionedCost = calculateProvisionedCost(125, 40)
console.log(`Monthly costs:`)
console.log(` On-demand: $${onDemandCost.toFixed(2)}`)
console.log(` Provisioned: $${provisionedCost.toFixed(2)}`)
console.log(` Savings: $${(onDemandCost - provisionedCost).toFixed(2)} (${((1 - provisionedCost / onDemandCost) * 100).toFixed(0)}%)`)
// Output:
// Monthly costs:
// On-demand: $129.75
// Provisioned: $3.28
// Savings: $126.47 (97%)
Cost Savings
For steady, predictable workloads, provisioned capacity can save 90%+ compared to on-demand!
Auto-scaling configuration¶
Setting up auto-scaling¶
// Configure auto-scaling for provisioned capacity
// (typically done via AWS Console, CloudFormation, or CDK)
const autoScalingConfig = {
minCapacity: 5, // Minimum RCU/WCU
maxCapacity: 100, // Maximum RCU/WCU
targetUtilization: 70, // Target 70% utilization
scaleInCooldown: 60, // Wait 60s before scaling in
scaleOutCooldown: 60 // Wait 60s before scaling out
}
// Auto-scaling will:
// - Scale up when utilization > 70% for 2 consecutive minutes
// - Scale down when utilization < 70% for 15 consecutive minutes
Monitoring auto-scaling¶
// Monitor capacity utilization
import { CloudWatch } from '@aws-sdk/client-cloudwatch'
const cloudwatch = new CloudWatch({})
async function getCapacityUtilization(tableName: string) {
const metrics = await cloudwatch.getMetricStatistics({
Namespace: 'AWS/DynamoDB',
MetricName: 'ConsumedReadCapacityUnits',
Dimensions: [{ Name: 'TableName', Value: tableName }],
StartTime: new Date(Date.now() - 3600000), // Last hour
EndTime: new Date(),
Period: 300, // 5 minutes
Statistics: ['Average', 'Maximum']
})
return metrics.Datapoints
}
Monitoring and optimization¶
Key metrics to monitor¶
// Use stats collector to track capacity usage
const stats = new StatsCollector()
// After operations
const summary = stats.getSummary()
console.log('Capacity metrics:')
console.log(` Total RCU consumed: ${summary.totalRCU}`)
console.log(` Total WCU consumed: ${summary.totalWCU}`)
console.log(` Peak RCU/sec: ${summary.peakRCU}`)
console.log(` Peak WCU/sec: ${summary.peakWCU}`)
console.log(` Average item size: ${summary.avgItemSize} bytes`)
Identifying optimization opportunities¶
import { RecommendationEngine } from '@ddb-lib/stats'
const recommendations = new RecommendationEngine(stats)
const capacityRecommendations = recommendations.getRecommendations()
.filter(r => r.type === 'CAPACITY')
for (const rec of capacityRecommendations) {
console.log(`${rec.message}`)
console.log(` Impact: ${rec.impact}`)
console.log(` Suggestion: ${rec.suggestion}`)
}
// Example output:
// "Table is over-provisioned"
// Impact: "Wasting $50/month"
// Suggestion: "Reduce RCU from 100 to 50"
Capacity planning strategies¶
Strategy 1: start with on-demand¶
// For new applications, start with on-demand
const table = new TableClient({
tableName: 'NewApp',
billingMode: 'PAY_PER_REQUEST'
})
// Monitor for 1-2 weeks to understand patterns
// Then switch to provisioned if traffic is steady
Strategy 2: use reserved capacity¶
// For predictable, long-term workloads
// Purchase reserved capacity for 1-3 years
// Saves up to 76% compared to provisioned
// Reserved capacity pricing (approximate):
// 1-year: 43% savings
// 3-year: 76% savings
// Example: 100 RCU reserved for 1 year
const standardCost = 100 * 0.00013 * 730 * 12 // $113.88/year
const reservedCost = standardCost * 0.57 // $64.91/year
const savings = standardCost - reservedCost // $48.97/year
Strategy 3: separate hot and cold data¶
// Use different tables for different access patterns
// Hot data: frequently accessed, provisioned capacity
const hotTable = new TableClient({
tableName: 'HotData',
billingMode: 'PROVISIONED',
readCapacity: 100,
writeCapacity: 50
})
// Cold data: rarely accessed, on-demand
const coldTable = new TableClient({
tableName: 'ColdData',
billingMode: 'PAY_PER_REQUEST'
})
// Archive old data to cold table
// Saves costs while maintaining access
Strategy 4: use GSI projections wisely¶
// GSIs consume additional capacity
// Project only needed attributes to reduce costs
// Bad: Project all attributes
const gsiConfig = {
indexName: 'StatusIndex',
projectionType: 'ALL' // Doubles storage and capacity costs
}
// Good: Project only needed attributes
const gsiConfig = {
indexName: 'StatusIndex',
projectionType: 'INCLUDE',
nonKeyAttributes: ['name', 'email'] // Only what's needed
}
Handling traffic spikes¶
Burst capacity¶
DynamoDB provides burst capacity for temporary spikes:
- Burst capacity: 300 seconds of unused capacity
- Automatically available, no configuration needed
- Helps handle short-term spikes
// Example: Provisioned 10 RCU
// Unused capacity accumulates up to 3000 RCU (10 * 300)
// Can burst to 30 RCU/sec for 100 seconds
Adaptive capacity¶
DynamoDB automatically redistributes capacity for hot partitions:
- Isolates frequently accessed items
- Provides additional throughput automatically
- No configuration needed
Burst and Adaptive Capacity
While burst and adaptive capacity help with spikes, don't rely on them for sustained traffic. Provision adequate capacity or use on-demand mode.
Common mistakes to avoid¶
❌ mistake 1: under-provisioning¶
// Bad: Provisioning exactly for average load
const avgRCU = 50
const provisionedRCU = 50 // No buffer!
// Good: Add buffer for spikes
const provisionedRCU = Math.ceil(avgRCU * 1.25) // 25% buffer
❌ mistake 2: not monitoring utilization¶
// Bad: Set and forget
// Provision capacity and never check again
// Good: Monitor and adjust
setInterval(async () => {
const utilization = await getCapacityUtilization('MyTable')
if (utilization.average < 0.3) {
console.log('Consider reducing capacity')
}
}, 86400000) // Check daily
❌ mistake 3: using on-demand for steady traffic¶
// Bad: Using on-demand for predictable workload
// Costs 5-10x more than provisioned
// Good: Switch to provisioned after understanding patterns
// Monitor for 1-2 weeks, then switch if traffic is steady
Key takeaways¶
- Choose the right mode - on-demand for unpredictable, provisioned for steady traffic
- Calculate requirements - understand your access patterns and item sizes
- Add buffer capacity - 20-30% buffer for traffic spikes
- Monitor continuously - track utilization and adjust as needed
- Optimize costs - use reserved capacity, projections, and separate hot/cold data
Related best practices¶
- Query vs scan - Efficient queries reduce capacity needs
- Projection expressions - Reduce RCU consumption
- Batch operations - Maximize throughput efficiency
Related guides¶
- Monitoring - Track capacity usage with stats
- Core operations - Understand operation costs
- Access patterns - Design efficient access patterns