DynamoDB’s on-demand capacity mode, while seemingly convenient, is often the silent killer of your budget, charging you per read/write operation regardless of actual usage patterns.

Let’s say you have a critical service that experiences spiky traffic. It needs to handle sudden bursts of reads and writes, but most of the time, it’s relatively quiet.

Here’s how that looks in practice with on-demand:

{
  "TableName": "MySpikyServiceTable",
  "ProvisionedThroughput": {
    "ReadCapacityUnits": 0,
    "WriteCapacityUnits": 0
  },
  "BillingMode": "PAY_PER_REQUEST"
}

When a burst hits, say 10,000 RCUs and 5,000 WCUs for 5 minutes, and then drops back to 100 RCUs and 50 WCUs, you’re paying for every single one of those operations at the on-demand rate (which is higher than provisioned rates). Over a month, those peak bursts, even if infrequent, can add up significantly.

The core problem with on-demand for predictable or semi-predictable workloads is its lack of cost optimization for consistent throughput. You’re paying a premium for elasticity you might not always need.

The Fix: Shifting to Provisioned Capacity

For tables with predictable traffic patterns, even if those patterns have peaks, switching to provisioned capacity is the first step.

  1. Analyze Your Traffic: Use CloudWatch metrics for your DynamoDB table (ConsumedReadCapacityUnits, ConsumedWriteCapacityUnits) over a sufficient period (e.g., 1-2 weeks, ideally a month) to identify your average and peak throughput requirements. Look for patterns: daily, weekly, monthly.

  2. Set Provisioned Throughput: Based on your analysis, provision your ReadCapacityUnits (RCUs) and WriteCapacityUnits (WCUs) to meet your sustained peak load. If your peak is 500 RCUs and 100 WCUs for 95% of the time, provision for that.

    aws dynamodb update-table \
        --table-name MySpikyServiceTable \
        --provisioned-throughput ReadCapacityUnits=500,WriteCapacityUnits=100
    

    This directly reduces your per-operation cost. You’re now paying a flat rate for the provisioned capacity, which is cheaper than the on-demand rate.

  3. Enable Auto Scaling: To handle the spikes without over-provisioning constantly, configure DynamoDB Auto Scaling. This automatically adjusts your provisioned capacity based on actual consumption, within defined minimum and maximum limits.

    You’ll define TargetTracking scaling policies. For example, to keep average consumed RCUs at 70% of provisioned capacity:

    aws application-autoscaling put-scaling-policy \
        --service-namespace dynamodb \
        --scalable-dimension dynamodb:table:ReadCapacityUnits \
        --policy-name MyTableReadAutoScaling \
        --policy-type TargetTrackingScaling \
        --target-tracking-scaling-policy-configuration '{
            "TargetValue": 70.0,
            "PredefinedMetricSpecification": {
                "PredefinedMetricType": "DynamoDBReadCapacityUtilization"
            },
            "ScaleInCooldown": 300,
            "ScaleOutCooldown": 300,
            "DisablingTimerForOnDemand": 300
        }'
    
    aws application-autoscaling put-scaling-policy \
        --service-namespace dynamodb \
        --scalable-dimension dynamodb:table:WriteCapacityUnits \
        --policy-name MyTableWriteAutoScaling \
        --policy-type TargetTrackingScaling \
        --target-tracking-scaling-policy-configuration '{
            "TargetValue": 70.0,
            "PredefinedMetricSpecification": {
                "PredefinedMetricType": "DynamoDBWriteCapacityUtilization"
            },
            "ScaleInCooldown": 300,
            "ScaleOutCooldown": 300,
            "DisablingTimerForOnDemand": 300
        }'
    

    This means if your provisioned capacity is 500 RCUs and you’re consuming 350 RCUs (70%), Auto Scaling keeps it there. If consumption spikes to 450 RCUs, it will scale up provisioned capacity to meet that demand (e.g., to ~640 RCUs). When it drops, it scales back down. You pay for the provisioned capacity, but Auto Scaling ensures you have enough to handle peaks without paying for idle provisioned capacity during lulls.

The Next Level: Reserved Capacity

If you have a very predictable, long-term workload that consistently uses a significant amount of throughput, Reserved Capacity offers even deeper discounts.

  1. Identify Consistent Usage: Look at your average provisioned capacity over a month or quarter. If you consistently provision 1,000 RCUs and 500 WCUs for a specific table or set of tables, this is a candidate for Reserved Capacity.

  2. Purchase Reserved Capacity: You can purchase Reserved Capacity for 1 or 3 years. This commits you to a certain level of throughput for that duration but provides substantial savings (up to 40% or more compared to on-demand, and 20-30% compared to provisioned).

    aws dynamodb purchase-reserved-capacity \
        --reserved-capacity-offering-id <your-offering-id> \
        --table-name MySpikyServiceTable \
        --capacity-type "PROVISIONED" \
        --read-capacity-units 1000 \
        --write-capacity-units 500 \
        --payment-option "ALL_UPFRONT" # or PARTIAL_UPFRONT, NO_UPFRONT
    

    Reserved Capacity is applied to your account and then utilized by your provisioned tables. It’s a financial commitment for a guaranteed discount.

The Cleanup Crew: Time-To-Live (TTL)

Even with optimized capacity, if your table stores data that naturally expires, you’re still paying for storage and potentially reads/writes to data you no longer need.

  1. Identify Expiring Data: Does your table contain data that is only relevant for a specific period (e.g., session data, logs, temporary state)?

  2. Configure TTL: Set a TTL attribute on your table. This attribute must be a Number type and contain a Unix epoch timestamp (seconds). DynamoDB will automatically delete items where the TTL attribute’s value is in the past.

    aws dynamodb update-table \
        --table-name MySessionDataTable \
        --time-to-live-specification Enabled=true,AttributeName=ttlTimestamp
    

    Here, ttlTimestamp is the attribute containing the expiry time. When an item’s ttlTimestamp is, say, 1678886400 (March 15, 2023 12:00:00 PM UTC), and the current time is past that, DynamoDB will eventually delete the item. The deletion itself consumes write capacity, but it’s generally more efficient than manual deletion for large volumes of expiring data. Crucially, you stop paying for storage and any future access to that expired data.

    The cost savings from TTL come from two places: reduced storage costs and, more importantly, the elimination of read/write operations that would have been performed on stale data. This also frees up capacity that can be utilized by active data.

By combining these strategies:

  • On-Demand: Use only for truly unpredictable, low-volume, or entirely new tables where you’re still learning traffic patterns.
  • Provisioned + Auto Scaling: The workhorse for most established tables with predictable-to-spiky traffic.
  • Reserved Capacity: For predictable, high-volume, long-term workloads.
  • TTL: To automatically prune stale data and reduce storage and operational overhead.

The next thing you’ll worry about is the cost of data transfer out of DynamoDB if your application is in a different region or on-premises.

Want structured learning?

Take the full Dynamodb course →