ProvisionedThroughputExceededException in DynamoDB: Causes and Fixes
2026-03-25 · 9 min read
Your application is running fine at 2 PM. By 3 PM, response times spike, errors pile up, and your logs fill with this:
An error occurred (ProvisionedThroughputExceededException) when calling the
PutItem operation: The level of configured provisioned throughput for the table
or one or more global secondary indexes was exceeded. Consider increasing the
provisioned throughput for the under-provisioned index or table.
Or from the SDK:
DynamoDBServiceException: Rate exceeded for shard.
Throughput exceeds the current capacity of your table or index.
This error means DynamoDB is rejecting your requests because you are sending more read or write traffic than your table can handle. But the real question is why — and the answer is rarely as simple as "increase the capacity." In most cases, the root cause is a design problem that more capacity will not fix.
Here is how to diagnose and resolve this properly.
Step 1: Understand Your Current Table Configuration
Start by checking what capacity mode your table is using and what the current settings are:
aws dynamodb describe-table \
--table-name my-table \
--query 'Table.{
TableName: TableName,
BillingMode: BillingModeSummary.BillingMode,
ProvisionedThroughput: ProvisionedThroughput,
TableSizeBytes: TableSizeBytes,
ItemCount: ItemCount,
GSIs: GlobalSecondaryIndexes[*].{
IndexName: IndexName,
ProvisionedThroughput: ProvisionedThroughput
}
}'
The output shows whether you are in PROVISIONED or PAY_PER_REQUEST (on-demand) mode. If you are in provisioned mode, the ReadCapacityUnits and WriteCapacityUnits values tell you your ceiling.
A critical detail many teams miss: each Global Secondary Index (GSI) has its own provisioned throughput that is independent of the base table. Even if the base table has plenty of capacity, a throttled GSI can back-propagate and throttle writes to the base table.
Step 2: Check CloudWatch Metrics for Throttling
CloudWatch tells you exactly where and when throttling is happening:
# Check throttled read requests on the base table
aws cloudwatch get-metric-statistics \
--namespace AWS/DynamoDB \
--metric-name ReadThrottleEvents \
--dimensions Name=TableName,Value=my-table \
--start-time "2026-03-25T14:00:00Z" \
--end-time "2026-03-25T16:00:00Z" \
--period 300 \
--statistics Sum \
--query 'Datapoints[*].[Timestamp,Sum]' \
--output table
# Check throttled write requests
aws cloudwatch get-metric-statistics \
--namespace AWS/DynamoDB \
--metric-name WriteThrottleEvents \
--dimensions Name=TableName,Value=my-table \
--start-time "2026-03-25T14:00:00Z" \
--end-time "2026-03-25T16:00:00Z" \
--period 300 \
--statistics Sum \
--query 'Datapoints[*].[Timestamp,Sum]' \
--output table
Also check the consumed vs. provisioned capacity to understand utilization:
# Consumed write capacity
aws cloudwatch get-metric-statistics \
--namespace AWS/DynamoDB \
--metric-name ConsumedWriteCapacityUnits \
--dimensions Name=TableName,Value=my-table \
--start-time "2026-03-25T14:00:00Z" \
--end-time "2026-03-25T16:00:00Z" \
--period 300 \
--statistics Sum Average Maximum \
--query 'Datapoints | sort_by(@, &Timestamp)' \
--output table
If the consumed capacity is well below the provisioned capacity but you are still getting throttled, the problem is almost certainly hot partitions.
Root Cause 1: Hot Partition Keys
This is the most common cause and the most misunderstood. DynamoDB distributes data across partitions based on the partition key hash. If a disproportionate amount of traffic goes to the same partition key, that single partition gets overwhelmed while other partitions sit idle.
A classic example: using a date string like 2026-03-25 as the partition key. All of today's writes hit one partition. Yesterday's partition is cold. Tomorrow's does not exist yet.
Another example: a multi-tenant application using tenant_id as the partition key where one tenant generates 90% of the traffic. That tenant's partition is overloaded.
To identify hot partitions, use CloudWatch Contributor Insights:
# Enable Contributor Insights on the table
aws dynamodb update-contributor-insights \
--table-name my-table \
--contributor-insights-action ENABLE
After enabling it, check the most frequently accessed partition keys:
aws cloudwatch get-metric-data \
--metric-data-queries '[
{
"Id": "hotkeys",
"MetricStat": {
"Metric": {
"Namespace": "AWS/DynamoDB",
"MetricName": "ConsumedWriteCapacityUnits",
"Dimensions": [
{"Name": "TableName", "Value": "my-table"}
]
},
"Period": 60,
"Stat": "Maximum"
}
}
]' \
--start-time "2026-03-25T14:00:00Z" \
--end-time "2026-03-25T16:00:00Z"
The fix is to redesign your partition key to distribute traffic more evenly. Common strategies include:
- Adding a random suffix: Instead of
tenant_id, usetenant_id#shard_Nwhere N is a random number between 0 and 9. This spreads writes across 10 partitions per tenant. - Using a composite key: Combine the date with another attribute, like
2026-03-25#user_id. - Write sharding: For high-velocity write patterns, append a calculated shard number to the partition key and use scatter-gather for reads.
Root Cause 2: Insufficient Provisioned Capacity
Sometimes the answer really is that you need more capacity. If your consumed capacity consistently approaches or exceeds provisioned capacity, you need to scale up.
Quick fix for immediate relief:
# Increase provisioned capacity
aws dynamodb update-table \
--table-name my-table \
--provisioned-throughput ReadCapacityUnits=1000,WriteCapacityUnits=500
Or switch to on-demand mode to eliminate throttling entirely:
aws dynamodb update-table \
--table-name my-table \
--billing-mode PAY_PER_REQUEST
Important caveats about on-demand mode:
- You can switch from provisioned to on-demand once per day
- On-demand mode still has per-partition limits (3,000 RCU and 1,000 WCU per partition)
- On-demand is more expensive per request than provisioned if your traffic is predictable
- New on-demand tables start with a previous peak of 0 and scale gradually. If you switch and immediately send high traffic, you will still get throttled until the table adapts
If you stay on provisioned mode, enable auto-scaling:
# Register the scalable target
aws application-autoscaling register-scalable-target \
--service-namespace dynamodb \
--resource-id "table/my-table" \
--scalable-dimension "dynamodb:table:WriteCapacityUnits" \
--min-capacity 10 \
--max-capacity 5000
# Create the scaling policy
aws application-autoscaling put-scaling-policy \
--service-namespace dynamodb \
--resource-id "table/my-table" \
--scalable-dimension "dynamodb:table:WriteCapacityUnits" \
--policy-name "my-table-write-scaling" \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "DynamoDBWriteCapacityUtilization"
},
"ScaleInCooldown": 60,
"ScaleOutCooldown": 60
}'
Root Cause 3: GSI Throttling Back-Propagation
This is the sneakiest cause. When a Global Secondary Index is throttled, DynamoDB throttles writes to the base table to prevent the GSI from falling too far behind. Your base table has plenty of capacity, but writes are rejected because the GSI cannot keep up.
Check GSI-specific metrics:
aws cloudwatch get-metric-statistics \
--namespace AWS/DynamoDB \
--metric-name WriteThrottleEvents \
--dimensions Name=TableName,Value=my-table Name=GlobalSecondaryIndexName,Value=my-gsi \
--start-time "2026-03-25T14:00:00Z" \
--end-time "2026-03-25T16:00:00Z" \
--period 300 \
--statistics Sum \
--query 'Datapoints[*].[Timestamp,Sum]' \
--output table
The fix: ensure every GSI has at least as much write capacity as the base table. Better yet, set GSI write capacity higher than the base table, because GSI writes include the overhead of index maintenance.
Root Cause 4: Burst Capacity Exhaustion
DynamoDB provides 300 seconds of burst capacity — unused capacity that accumulates when your table is underutilized. During traffic spikes, DynamoDB draws from this reserve. Once depleted, strict throttling kicks in.
If you see throttling only during brief spikes, your burst capacity may be exhausted. The telltale sign is throttling that stops after a few minutes of reduced traffic.
There is no CLI command to check burst capacity directly — it is not exposed as a metric. But you can infer it: if ConsumedCapacity is below ProvisionedCapacity for extended periods and then briefly exceeds it before throttling starts, burst capacity was the buffer that ran out.
Root Cause 5: Adaptive Capacity Not Yet Activated
DynamoDB's adaptive capacity feature automatically redistributes provisioned capacity to hot partitions. But it takes 5 to 30 minutes to kick in. If a sudden traffic pattern change creates a new hot partition, you may see throttling until adaptive capacity adjusts.
This is a transient issue. If the throttling resolves itself after 15-30 minutes, adaptive capacity was likely the cause. No action needed beyond ensuring your retry logic handles the temporary throttling gracefully.
Implementing Proper Retry Logic
Regardless of the root cause, your application must handle throttling gracefully. The AWS SDKs include built-in retry logic, but the defaults may not be aggressive enough:
import boto3
from botocore.config import Config
# Configure aggressive retries with exponential backoff
config = Config(
retries={
'max_attempts': 10,
'mode': 'adaptive' # adaptive mode adjusts to throttling patterns
}
)
dynamodb = boto3.resource('dynamodb', config=config)
table = dynamodb.Table('my-table')
For batch operations, implement exponential backoff on UnprocessedItems:
import time
import random
def batch_write_with_backoff(table_name, items, max_retries=8):
client = boto3.client('dynamodb')
unprocessed = {table_name: items}
retries = 0
while unprocessed and retries < max_retries:
response = client.batch_write_item(RequestItems=unprocessed)
unprocessed = response.get('UnprocessedItems', {})
if unprocessed:
retries += 1
wait_time = min(2 ** retries + random.uniform(0, 1), 30)
print(f"Retry {retries}: {len(unprocessed[table_name])} unprocessed items, "
f"waiting {wait_time:.1f}s")
time.sleep(wait_time)
return unprocessed
Prevention Best Practices
-
Choose partition keys that distribute traffic evenly. High-cardinality attributes like user IDs are better than low-cardinality ones like status codes or dates.
-
Monitor CloudWatch throttle metrics with alarms. Do not wait for application errors to discover throttling:
aws cloudwatch put-metric-alarm \
--alarm-name "DynamoDB-Throttle-my-table" \
--namespace AWS/DynamoDB \
--metric-name WriteThrottleEvents \
--dimensions Name=TableName,Value=my-table \
--statistic Sum \
--period 300 \
--threshold 10 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:us-east-1:123456789012:my-alerts
-
Enable Contributor Insights on all production tables. The cost is minimal and it gives you instant visibility into hot keys.
-
Use DAX (DynamoDB Accelerator) for read-heavy workloads. DAX caches frequently accessed items and reduces read load on the base table by 10x or more.
-
Consider on-demand mode for unpredictable workloads. The per-request cost is higher, but you never get throttled (within partition limits) and there is no capacity planning required.
When Throttling Is an Architecture Problem
If you find yourself constantly adjusting capacity settings or fighting hot partition issues, the problem may be your data model. DynamoDB excels with access patterns that are known and designed for in advance. If your access patterns have evolved beyond what your table design supports, it may be time for a data model redesign.
We regularly help teams redesign their DynamoDB tables and access patterns to eliminate throttling issues permanently. If your DynamoDB throttling is impacting user experience or costing you in over-provisioned capacity, reach out for a free architecture review — we will analyze your table design and access patterns and show you exactly where the bottlenecks are.
Need help with your AWS infrastructure?
Book a free 30-minute consultation to discuss your challenges.