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.
