Fix ProvisionedThroughputExceededException in AWS DynamoDB

intermediateโ˜๏ธ AWS2026-03-22| AWS DynamoDB, AWS SDK (Python/Boto3, Node.js, Java), AWS CLI โ€” any region

Error Message

ProvisionedThroughputExceededException: The level of configured provisioned throughput for the table was exceeded. Consider increasing your provisioning level with the UpdateTable API.
#dynamodb#throughput#capacity#aws#nosql

What's happening

DynamoDB is rejecting your requests because your table can't keep up with the traffic. Every table has a fixed read/write budget measured in RCU (Read Capacity Units) and WCU (Write Capacity Units). Blow past that budget โ€” even for a fraction of a second โ€” and DynamoDB throttles the request immediately:

ProvisionedThroughputExceededException: The level of configured provisioned throughput for the table was exceeded. Consider increasing your provisioning level with the UpdateTable API.

The usual suspects: sudden traffic spikes, a bulk import job, or a Global Secondary Index (GSI) provisioned with less capacity than the base table. Let's find out which one.

Step 1 โ€” Find out what's being throttled

Don't touch the capacity settings yet. Check CloudWatch first to see where the bottleneck is.

# List throttle metrics for a specific table
aws cloudwatch get-metric-statistics \
  --namespace AWS/DynamoDB \
  --metric-name ReadThrottleEvents \
  --dimensions Name=TableName,Value=YourTableName \
  --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
  --period 300 \
  --statistics Sum

Run this for both ReadThrottleEvents and WriteThrottleEvents. GSIs have their own throttle counters โ€” add Name=GlobalSecondaryIndexName,Value=YourIndexName to the dimensions to check them separately.

Then pull the current provisioned numbers:

aws dynamodb describe-table --table-name YourTableName \
  --query 'Table.{RCU:ProvisionedThroughput.ReadCapacityUnits, WCU:ProvisionedThroughput.WriteCapacityUnits, BillingMode:BillingModeSummary}'

Now you have a baseline. A spike of 500+ throttle events in 5 minutes is a different problem than a slow, steady 10/minute leak.

Step 2 โ€” Add exponential backoff (fastest fix)

If you're calling DynamoDB without retry logic, that's your first problem โ€” not the capacity. The AWS SDK has built-in exponential backoff. Make sure it's turned on and configured aggressively.

Python (Boto3):

import boto3
from botocore.config import Config

config = Config(
    retries={
        'max_attempts': 10,
        'mode': 'adaptive'  # backs off automatically on throttling
    }
)

dynamodb = boto3.resource('dynamodb', config=config)
table = dynamodb.Table('YourTableName')

Node.js (AWS SDK v3):

import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

const client = new DynamoDBClient({
  maxAttempts: 10, // SDK default is 3 โ€” too low for throttled workloads
});

mode: 'adaptive' in Boto3 is the smarter choice โ€” it measures real-time error rates and adjusts retry timing accordingly. For most transient throttling, this alone stops the exceptions without touching capacity at all.

Step 3a โ€” Increase provisioned capacity

Throttling is sustained, not just a one-time spike? Time to raise the ceiling. You can do this via CLI in seconds:

# Bump write capacity to 100 WCU
aws dynamodb update-table \
  --table-name YourTableName \
  --provisioned-throughput ReadCapacityUnits=50,WriteCapacityUnits=100

For a GSI with its own throughput settings:

aws dynamodb update-table \
  --table-name YourTableName \
  --global-secondary-index-updates \
    '[{"Update":{"IndexName":"YourGSIName","ProvisionedThroughput":{"ReadCapacityUnits":50,"WriteCapacityUnits":50}}}]'

Capacity changes propagate in seconds. One hard limit to know: AWS allows up to 4 decreases per table per calendar day (UTC). Increases are unlimited, so scale up freely.

Step 3b โ€” Switch to on-demand mode (simpler option)

Unpredictable traffic? Skip capacity planning entirely. On-demand mode lets DynamoDB handle any request rate automatically.

aws dynamodb update-table \
  --table-name YourTableName \
  --billing-mode PAY_PER_REQUEST

The trade-off: on-demand costs roughly 6-7x more per million requests than well-tuned provisioned capacity. For a table doing 10M reads/day at steady state, that difference adds up. But for workloads with sharp peaks and long quiet periods, it often works out cheaper than over-provisioning. Switch back to provisioned once you understand your actual traffic pattern.

Step 3c โ€” Enable auto scaling (best of both worlds)

Auto scaling watches utilization and adjusts provisioned capacity in real time. A target of 70% gives you a 30% headroom buffer for sudden spikes before throttling kicks in.

# Register table as a scalable target
aws application-autoscaling register-scalable-target \
  --service-namespace dynamodb \
  --resource-id "table/YourTableName" \
  --scalable-dimension "dynamodb:table:WriteCapacityUnits" \
  --min-capacity 5 \
  --max-capacity 500

# Set the scaling policy
aws application-autoscaling put-scaling-policy \
  --service-namespace dynamodb \
  --resource-id "table/YourTableName" \
  --scalable-dimension "dynamodb:table:WriteCapacityUnits" \
  --policy-name "WriteAutoScaling" \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration \
    '{"TargetValue": 70.0, "PredefinedMetricSpecification": {"PredefinedMetricType": "DynamoDBWriteCapacityUtilization"}}'

Note that auto scaling reacts to sustained load โ€” it typically takes 1-2 minutes to kick in. It won't save you from a traffic spike that slams the table in the first 60 seconds.

Step 4 โ€” Reduce hot partition pressure

Sometimes total capacity isn't the problem. A single partition is. DynamoDB splits data by hash key โ€” when many requests target the same key, that one partition maxes out while the others sit idle. You can have 1000 WCU provisioned and still throttle.

  • Avoid sequential or monotonic keys โ€” auto-increment IDs and Unix timestamps as partition keys are notorious hot partition causes. Use UUIDs or append a random suffix like userId#shard-3.
  • Spread bulk writes โ€” if you're importing 100k rows, scatter them across different partition keys rather than writing them in order.
  • Add DAX for read-heavy workloads โ€” DynamoDB Accelerator is an in-memory cache that sits in front of DynamoDB and absorbs read spikes at microsecond latency. A DAX cluster with 2 nodes can handle tens of millions of reads per second.
  • Batch your writes โ€” BatchWriteItem packs up to 25 writes into a single API call. Combine it with rate limiting to stay under capacity:
# Python: batch write with rate limiting
import time

def batch_write_with_limit(table, items, wcu_limit=50):
    chunk_size = 25  # DynamoDB max items per BatchWriteItem
    for i in range(0, len(items), chunk_size):
        chunk = items[i:i+chunk_size]
        with table.batch_writer() as batch:
            for item in chunk:
                batch.put_item(Item=item)
        time.sleep(1)  # pause 1s between chunks to throttle throughput

Verify the fix

Once you've made changes, pull the last 5 minutes of throttle data:

aws cloudwatch get-metric-statistics \
  --namespace AWS/DynamoDB \
  --metric-name WriteThrottleEvents \
  --dimensions Name=TableName,Value=YourTableName \
  --start-time $(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%SZ) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
  --period 60 \
  --statistics Sum

A count of 0 means clean. Also compare ConsumedWriteCapacityUnits against ProvisionedWriteCapacityUnits โ€” if you're consistently above 80%, you're one traffic spike away from throttling again. Keep utilization under 80% for sustained loads.

Key takeaways

  • Configure adaptive retries first. It's free, takes 2 minutes, and absorbs most transient throttling without touching capacity.
  • Set CloudWatch alarms on throttle events. Alarm on ReadThrottleEvents and WriteThrottleEvents with a threshold of 10+ per minute โ€” catch problems before users report them.
  • GSI capacity is independent from the base table. A heavily-queried index with low WCU is a common silent culprit that's easy to miss.
  • On-demand isn't always the expensive choice. For tables that are idle 20 hours a day but handle big bursts during peak hours, PAY_PER_REQUEST often beats over-provisioned capacity on cost.

Related Error Notes