Cloud Horizon Get the free audit

May 8, 2026 9 min read

DynamoDB: the on-demand vs provisioned tipping point

On-demand DynamoDB is roughly 7x the per-request cost of fully-utilized provisioned. The tipping point is 14 to 18 percent sustained utilization. Below, on-demand wins. Above, switch and turn on autoscaling. The math, the GSI multiplier, the audit query.

DynamoDB has two billing modes and one number that decides which is cheaper. On-demand charges per million requests. Provisioned charges per RCU and WCU per hour, regardless of usage. The arithmetic across 730 hours says on-demand is roughly 7x the per-request cost of provisioned at full utilization. The tipping point is the utilization where the two break even, and on most workloads it lands between 14 and 18 percent.

Stay below that utilization and on-demand is cheaper, even accounting for the spikes provisioned cannot serve without throttling. Stay above it and provisioned wins by margins that compound across GSIs. The first audit step on any DynamoDB bill is to find every table whose mode disagrees with its actual utilization curve.

The pricing in one paragraph

Provisioned in us-east-1 is $0.00013 per RCU-hour and $0.00065 per WCU-hour. One RCU covers one strongly-consistent read of up to 4 KB per second, or two eventually-consistent reads. One WCU covers one write of up to 1 KB per second. On-demand is $0.125 per million read request units and $0.625 per million write request units, where one RRU is a 4 KB strongly-consistent read and one WRU is a 1 KB write. Storage is $0.25 per GB-month on Standard, $0.10 on Standard-IA. PITR is $0.20 per GB-month. Streams are $0.20 per million change reads.

The GSI multiplier

Every Global Secondary Index has its own RCU, WCU, and storage. Every write to the base table replicates to every GSI, so a table with three GSIs writes four times. The cost of a GSI is identical to a separate table holding the same data. The audit pattern: list every table, list its GSIs, ask when each GSI was last queried by the application code, and be ready to find one that no living code path needs.

The audit query

aws dynamodb list-tables --query 'TableNames' --output table

aws dynamodb describe-table --table-name mytable \
  --query 'Table.[BillingModeSummary.BillingMode,GlobalSecondaryIndexes[].IndexName,ProvisionedThroughput]' \
  --output table

# CloudWatch consumed capacity for the last 14 days, hourly
aws cloudwatch get-metric-statistics \
  --namespace AWS/DynamoDB \
  --metric-name ConsumedReadCapacityUnits \
  --dimensions Name=TableName,Value=mytable \
  --start-time $(date -u -d '14 days ago' +%FT%TZ) \
  --end-time $(date -u +%FT%TZ) \
  --period 3600 --statistics Sum --output json

The third command is the load-bearing one. ConsumedReadCapacityUnits on a 14-day window with a 3600-second period gives you the peak hour, the median hour, and everything between. Divide the peak by the provisioned RCU value and you get utilization. Median utilization below 18 percent means on-demand would be cheaper. Above means autoscaling at 70 percent target is the win.

Switching modes is an online operation

Switching billing mode does not require downtime, does not require code changes, does not interrupt traffic. The catch: you can only switch once every 24 hours per table. So pick carefully. The Terraform diff is a single argument:

billing_mode = "PAY_PER_REQUEST"  # or "PROVISIONED"

Autoscaling is the escape hatch from cold provisioned math

The reason teams pick on-demand on otherwise predictable workloads is the fear of getting provisioning wrong and throttling traffic. Autoscaling is the answer. Set a target utilization of 60 to 75 percent, set sane min and max, and let the service add and remove capacity every minute. The Terraform shape:

resource "aws_appautoscaling_target" "read" {
  max_capacity       = 1000
  min_capacity       = 50
  resource_id        = "table/${aws_dynamodb_table.main.name}"
  scalable_dimension = "dynamodb:table:ReadCapacityUnits"
  service_namespace  = "dynamodb"
}

resource "aws_appautoscaling_policy" "read" {
  name               = "read-target-70"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.read.resource_id
  scalable_dimension = aws_appautoscaling_target.read.scalable_dimension
  service_namespace  = aws_appautoscaling_target.read.service_namespace

  target_tracking_scaling_policy_configuration {
    target_value = 70.0
    predefined_metric_specification {
      predefined_metric_type = "DynamoDBReadCapacityUtilization"
    }
  }
}

With autoscaling, the practical utilization is 65 to 75 percent on healthy workloads, which is well past the tipping point. The savings vs on-demand on a 1,000 RCU table at this utilization run 4x to 5x.

The four other lines that hide on the bill

PITR on ephemeral tables. Sessions, rate-limit counters, work queues. Recovering them is meaningless. PITR adds 80 percent to storage cost on small tables and the audit is a single boto3 call.

Strongly-consistent reads where eventual is fine. Half the RCU per request. The default in the SDK is eventual for a reason. Search the codebase for ConsistentRead=true and challenge each one.

Item size growth. Each KB above 1 on a write bills another full WCU. A schema change that adds a 200 byte field to a 900 byte item silently doubles WCU consumption on a write-heavy table.

Standard-IA misuse. Standard-IA cuts storage by 60 percent and raises request rates by 25 percent. The break-even is roughly 50 reads or 50 writes per GB-month. Audit logs and archive tables are good candidates. Hot application tables are not.

The before-lunch change

Run the audit query on your largest table. If utilization is below 18 percent and the table is on provisioned, switch to on-demand. If utilization is above 18 percent and the table is on on-demand, switch to provisioned with autoscaling. Either change is a one-line Terraform diff and starts saving within 24 hours. Plug your numbers into the DynamoDB cost calculator first to see the dollar value.

Keep reading

More from the blog