|

DynamoDB: Consider Using On-Demand Tables Rather Than Provisioned in Non-Production Projects

This FinOps policy recommends switching DynamoDB tables in non-production environments from provisioned capacity mode to on-demand capacity mode. Provisioned capacity requires upfront read/write unit commitments that generate costs even when tables sit idle, making it a poor fit for development, staging, and testing workloads.

Why This Policy Matters

Non-production DynamoDB tables are rarely accessed at consistent, predictable rates. Provisioned capacity mode charges for reserved read capacity units (RCUs) and write capacity units (WCUs) continuously, regardless of actual usage.

When a development table sits idle overnight, on weekends, or between test runs, those provisioned units still accrue charges. This is a common source of wasted spend in organizations with multiple non-production environments.

On-demand capacity mode charges only for the actual read and write requests made against the table. For sporadic or unpredictable workloads, this directly reduces DynamoDB costs without any performance trade-off.

How It Helps Reduce Costs

Provisioned capacity requires teams to estimate peak throughput in advance. Overestimating is common in non-production environments where actual usage patterns are unknown or inconsistent.

On-demand mode removes this estimation requirement entirely. Costs scale with actual request volume, which in non-production environments is typically low and intermittent.

This approach reduces idle spend, eliminates over-provisioning waste, and removes the need to manage auto-scaling policies for tables that do not require predictable performance guarantees.

Potential Savings

A single DynamoDB table provisioned at 100 RCUs and 100 WCUs costs approximately $58/month in us-east-1, regardless of whether any requests are made.

An equivalent on-demand table used only during active development sessions, averaging 1 million reads and 500,000 writes per month, would cost roughly $1.50 to $3.00 in the same region.

Across 10 non-production tables, switching from provisioned to on-demand can reduce DynamoDB costs by 80% or more, depending on actual usage patterns.

Implementation Guide

Infrastructure-as-Code Example (Terraform)

The following example shows a DynamoDB table configured with provisioned capacity, which is the default Terraform behavior and a frequent source of unnecessary spend in non-production environments.

# Non-compliant: Provisioned capacity in a non-production environment
resource "aws_dynamodb_table" "dev_table" {
  name           = "dev-users"
  billing_mode   = "PROVISIONED"
  read_capacity  = 100
  write_capacity = 100
  hash_key       = "userId"

  attribute {
    name = "userId"
    type = "S"
  }
}

This configuration charges for 100 RCUs and 100 WCUs every hour, whether the table receives traffic or not. This is a common source of wasted spend in development and staging accounts.

# Compliant: On-demand capacity for non-production environments
resource "aws_dynamodb_table" "dev_table" {
  name         = "dev-users"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "userId"

  attribute {
    name = "userId"
    type = "S"
  }
}

Switching billing_mode to PAY_PER_REQUEST removes the read_capacity and write_capacity arguments. DynamoDB handles scaling automatically. There is no performance penalty for typical non-production workloads.

Using Terraform Workspaces or Variables for Environment Targeting

Teams managing multiple environments from a single Terraform configuration can use a variable to conditionally apply on-demand mode in non-production contexts.

variable "environment" {
  description = "Deployment environment (e.g., dev, staging, prod)"
  type        = string
}

locals {
  billing_mode   = var.environment == "prod" ? "PROVISIONED" : "PAY_PER_REQUEST"
  read_capacity  = var.environment == "prod" ? 100 : null
  write_capacity = var.environment == "prod" ? 100 : null
}

resource "aws_dynamodb_table" "users" {
  name           = "${var.environment}-users"
  billing_mode   = local.billing_mode
  read_capacity  = local.read_capacity
  write_capacity = local.write_capacity
  hash_key       = "userId"

  attribute {
    name = "userId"
    type = "S"
  }
}

This pattern enforces on-demand mode automatically for any environment that is not explicitly production. It reduces configuration drift and prevents accidental provisioned capacity in new non-production deployments.

Manual Step-by-Step Instructions

Step 1: Identify provisioned DynamoDB tables in non-production accounts.

Use the AWS CLI to list tables and check their billing mode:

aws dynamodb list-tables --query "TableNames[]" --output text | \
  xargs -I {} aws dynamodb describe-table --table-name {} \
  --query "Table.{Name:TableName, BillingMode:BillingModeSummary.BillingMode}"

Tables without a BillingMode field are running in provisioned mode by default.

Step 2: Evaluate actual usage.

Review CloudWatch metrics for ConsumedReadCapacityUnits and ConsumedWriteCapacityUnits over the last 30 days. If consumed capacity is consistently far below provisioned capacity, the table is over-provisioned.

Step 3: Update the Terraform configuration.

Change billing_mode to PAY_PER_REQUEST and remove read_capacity and write_capacity arguments from the resource block.

Step 4: Apply the change.

Run terraform plan to confirm the diff, then apply:

terraform plan -var="environment=dev"
terraform apply -var="environment=dev"

DynamoDB supports switching between billing modes with no downtime or data migration required.

Step 5: Validate the change.

Confirm in the AWS Console or via CLI that the table’s billing mode is now PAY_PER_REQUEST.

Step 6: Monitor costs.

Review DynamoDB line items in AWS Cost Explorer for the next billing cycle to confirm reduced spend.

Best Practices

  • Apply on-demand mode as the default for all non-production DynamoDB tables.
  • Use Terraform variables or workspace logic to enforce billing mode per environment.
  • Tag all DynamoDB tables with environment and team tags to enable cost allocation.
  • Avoid enabling DynamoDB auto-scaling in non-production environments; it adds configuration complexity without meaningful cost benefit at low usage volumes.
  • Review DynamoDB billing mode as part of quarterly non-production environment audits.
  • Set AWS Budgets alerts for DynamoDB spend in non-production accounts to catch provisioned capacity drift early.

Tools and Scripts to Help Implement

Infracost detects DynamoDB provisioned capacity configurations automatically during infrastructure cost estimation. When a Terraform plan includes a DynamoDB table with billing_mode = “PROVISIONED” in a non-production context, Infracost flags the cost impact and surfaces the projected monthly charge.

Infracost integrates directly into CI/CD pipelines. Every pull request that includes DynamoDB table changes receives an automated cost estimate, allowing engineers and FinOps reviewers to catch provisioned capacity before it is deployed.

This policy is available in Infracost, including in the free trial. Teams can use Infracost to detect existing violations across their Terraform codebase, prioritize remediation by cost impact, and track progress as tables are migrated to on-demand mode.

Infracost also supports custom policies, allowing platform teams to enforce this rule automatically as part of a policy-as-code workflow. Violations can be configured to block pull request merges or generate warnings, depending on the team’s enforcement preference.

Examples of Impact

Example 1: Development environment with multiple tables

A development team maintained eight DynamoDB tables in a shared dev account. Each table was provisioned at 50 RCUs and 50 WCUs. Total monthly cost: approximately $232. After switching all eight tables to on-demand mode, actual usage averaged 2 million total requests per month across all tables. Monthly cost dropped to under $5, a reduction of over 97%.

Example 2: Staging environment pre-launch

A staging environment replicated production DynamoDB tables with identical provisioned capacity to validate load tests. After load tests completed, the tables remained provisioned at production levels for six weeks before the next test cycle. Switching staging tables to on-demand between test cycles reduced monthly DynamoDB spend in that account by approximately $340.

Considerations and Caveats

On-demand is not always cheaper for sustained high throughput. If a non-production table is used for continuous load testing at high request volumes, provisioned capacity with auto-scaling may be more cost-effective. Evaluate actual request rates before switching.

Switching billing modes has a cooldown period. AWS allows switching between provisioned and on-demand modes, but each table can only switch billing modes once every 24 hours. Plan migrations accordingly.

On-demand does not support reserved capacity discounts. DynamoDB reserved capacity applies only to provisioned mode. This trade-off is acceptable for non-production use cases where cost predictability is less critical.

Performance behavior differs at cold start. On-demand tables scale throughput automatically but may experience throttling during sudden, extreme traffic spikes on new tables. This is rarely a concern in non-production environments but should be considered for staging environments that mirror production traffic patterns.

Table-level billing mode applies uniformly. Unlike some other AWS services, DynamoDB billing mode cannot be configured at the index level. All global secondary indexes on the table inherit the same billing mode.

Frequently Asked Questions (FAQs)

Yes. This policy is available in Infracost, including in the free trial. Infracost detects provisioned DynamoDB tables in Terraform configurations and reports their projected cost, enabling teams to identify and fix non-production over-provisioning before deployment.

Yes. AWS supports switching between billing modes on existing tables with no data migration and no downtime. The change takes effect within a few minutes. Note that each table can only switch billing modes once per 24-hour period.

Check CloudWatch metrics for ConsumedReadCapacityUnits and ConsumedWriteCapacityUnits. If consumed capacity is consistently below 20% of provisioned capacity, the table is a strong candidate for on-demand mode. Infracost can also surface the provisioned cost directly in your pull request workflow.

For typical non-production usage patterns, there is no meaningful performance difference. On-demand tables scale automatically and do not require capacity planning. The only scenario where performance could differ is during sudden, large-scale traffic spikes on tables with no prior request history.

Yes. The policy can be scoped to specific environments, accounts, or teams using Terraform variables, workspace logic, or tagging strategies. Infracost’s custom policy support allows platform teams to define enforcement rules that match their specific environment structure and FinOps maturity level.

Global secondary indexes on the table automatically switch to on-demand billing when the table billing mode changes. There is no separate configuration required. All indexes inherit the table-level billing mode.

Infracost provides cost estimates for every Terraform change in CI/CD. As teams migrate tables to on-demand mode, each pull request reflects the updated cost. FinOps teams can track the reduction in projected DynamoDB spend across pull requests over time, providing measurable progress toward cost optimization goals.