Fix ProvisionedThroughputExceededException trong AWS DynamoDB

intermediate☁️ AWS2026-03-22| AWS DynamoDB, AWS SDK (Python/Boto3, Node.js, Java), AWS CLI — mọi 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

Chuyện gì đang xảy ra

DynamoDB đang từ chối các request của bạn vì bảng không theo kịp lưu lượng truy cập. Mỗi bảng có một ngân sách đọc/ghi cố định được đo bằng RCU (Read Capacity Units)WCU (Write Capacity Units). Vượt quá ngân sách đó — dù chỉ trong một phần giây — DynamoDB sẽ lập tức throttle request:

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

Các nguyên nhân thường gặp: traffic tăng đột biến, một job import dữ liệu hàng loạt, hoặc một Global Secondary Index (GSI) được cấu hình với capacity thấp hơn bảng gốc. Hãy cùng xác định nguyên nhân cụ thể.

Bước 1 — Xác định phần nào đang bị throttle

Chưa cần chỉnh capacity ngay. Kiểm tra CloudWatch trước để tìm điểm nghẽn.

# Liệt kê các throttle metric cho một bảng cụ thể
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

Chạy lệnh này cho cả ReadThrottleEvents lẫn WriteThrottleEvents. Các GSI có bộ đếm throttle riêng — thêm Name=GlobalSecondaryIndexName,Value=YourIndexName vào dimensions để kiểm tra từng index riêng lẻ.

Sau đó lấy các thông số capacity hiện tại:

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

Bây giờ bạn đã có số liệu cơ sở. Một đợt tăng vọt hơn 500 throttle event trong 5 phút là vấn đề khác hẳn so với tình trạng rò rỉ đều đặn 10 event/phút.

Bước 2 — Thêm exponential backoff (cách xử lý nhanh nhất)

Nếu bạn đang gọi DynamoDB mà không có retry logic, đó mới là vấn đề cốt lõi — chứ không phải capacity. AWS SDK có sẵn cơ chế exponential backoff. Hãy đảm bảo nó đang được bật và cấu hình đủ mạnh.

Python (Boto3):

import boto3
from botocore.config import Config

config = Config(
    retries={
        'max_attempts': 10,
        'mode': 'adaptive'  # tự động backoff khi bị 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, // mặc định SDK là 3 — quá thấp cho workload bị throttle
});

mode: 'adaptive' trong Boto3 là lựa chọn thông minh hơn — nó đo tỷ lệ lỗi theo thời gian thực và điều chỉnh thời gian retry cho phù hợp. Với hầu hết các trường hợp throttling tạm thời, chỉ cần bước này là đủ ngăn exception mà không cần động đến capacity.

Bước 3a — Tăng provisioned capacity

Throttling kéo dài liên tục, không chỉ là đột biến một lần? Đã đến lúc nâng mức trần. Bạn có thể làm điều này qua CLI trong vài giây:

# Tăng write capacity lên 100 WCU
aws dynamodb update-table \
  --table-name YourTableName \
  --provisioned-throughput ReadCapacityUnits=50,WriteCapacityUnits=100

Đối với GSI có throughput settings riêng:

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

Thay đổi capacity có hiệu lực trong vài giây. Một giới hạn quan trọng cần biết: AWS cho phép tối đa 4 lần giảm mỗi bảng mỗi ngày theo lịch (UTC). Tăng thì không giới hạn, vì vậy hãy thoải mái scale up.

Bước 3b — Chuyển sang on-demand mode (lựa chọn đơn giản hơn)

Traffic khó đoán trước? Bỏ qua việc lập kế hoạch capacity. On-demand mode cho phép DynamoDB tự động xử lý bất kỳ mức request rate nào.

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

Đánh đổi là: on-demand có chi phí cao hơn khoảng 6-7 lần trên mỗi triệu request so với provisioned capacity được tinh chỉnh tốt. Với một bảng xử lý 10 triệu lần đọc/ngày ở trạng thái ổn định, sự chênh lệch này sẽ tích lũy đáng kể. Nhưng với các workload có peak cao và thời gian nhàn rỗi dài, on-demand thường rẻ hơn so với việc over-provision. Hãy chuyển lại sang provisioned sau khi bạn đã nắm rõ traffic pattern thực tế.

Bước 3c — Bật auto scaling (giải pháp tốt nhất)

Auto scaling theo dõi mức sử dụng và điều chỉnh provisioned capacity theo thời gian thực. Target ở mức 70% cho bạn vùng đệm 30% để xử lý các đợt tăng đột biến trước khi throttling xảy ra.

# Đăng ký bảng là 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

# Thiết lập 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"}}'

Lưu ý rằng auto scaling phản ứng với tải kéo dài — thường mất 1-2 phút để kích hoạt. Nó sẽ không cứu bạn khỏi một đợt traffic spike đánh thẳng vào bảng trong 60 giây đầu tiên.

Bước 4 — Giảm áp lực hot partition

Đôi khi tổng capacity không phải vấn đề. Một partition đơn lẻ mới là thủ phạm. DynamoDB phân chia dữ liệu theo hash key — khi nhiều request nhắm vào cùng một key, partition đó bị quá tải trong khi các partition khác lại rảnh rỗi. Bạn có thể đã provision 1000 WCU mà vẫn bị throttle.

  • Tránh dùng key tuần tự hoặc đơn điệu — auto-increment ID và Unix timestamp làm partition key là những nguyên nhân hot partition điển hình. Hãy dùng UUID hoặc thêm hậu tố ngẫu nhiên như userId#shard-3.
  • Trải đều các bulk write — nếu bạn đang import 100k bản ghi, hãy phân tán chúng ra các partition key khác nhau thay vì ghi theo thứ tự.
  • Thêm DAX cho workload đọc nhiều — DynamoDB Accelerator là một in-memory cache nằm trước DynamoDB, hấp thụ các đợt read spike với độ trễ micro giây. Một DAX cluster với 2 node có thể xử lý hàng chục triệu lượt đọc mỗi giây.
  • Gom các write thành batchBatchWriteItem đóng gói tối đa 25 lần ghi vào một lần gọi API. Kết hợp với rate limiting để giữ trong giới hạn capacity:
# Python: batch write với rate limiting
import time

def batch_write_with_limit(table, items, wcu_limit=50):
    chunk_size = 25  # DynamoDB tối đa 25 item mỗi lần 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)  # dừng 1 giây giữa các chunk để giới hạn throughput

Xác nhận đã khắc phục

Sau khi thực hiện thay đổi, lấy dữ liệu throttle của 5 phút gần nhất:

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

Kết quả 0 là hoàn toàn ổn. Cũng hãy so sánh ConsumedWriteCapacityUnits với ProvisionedWriteCapacityUnits — nếu bạn liên tục trên 80%, chỉ cần một đợt traffic spike là bạn lại bị throttle. Hãy giữ mức sử dụng dưới 80% đối với tải kéo dài.

Những điểm cần ghi nhớ

  • Cấu hình adaptive retry trước tiên. Miễn phí, mất 2 phút, và xử lý được hầu hết các throttling tạm thời mà không cần động đến capacity.
  • Thiết lập CloudWatch alarm cho throttle event. Đặt alarm cho ReadThrottleEventsWriteThrottleEvents với ngưỡng từ 10+ event/phút — phát hiện sự cố trước khi người dùng báo cáo.
  • Capacity của GSI độc lập với bảng gốc. Một index được truy vấn nhiều nhưng có WCU thấp là thủ phạm âm thầm thường bị bỏ qua.
  • On-demand không phải lúc nào cũng đắt hơn. Với các bảng nhàn rỗi 20 tiếng mỗi ngày nhưng xử lý lưu lượng lớn trong giờ cao điểm, PAY_PER_REQUEST thường rẻ hơn so với over-provision capacity.

Related Error Notes