← Back to DevBytes

Monitoring DynamoDB: Metrics, Alarms, and Dashboards

Understanding DynamoDB Monitoring

Amazon DynamoDB is a fully managed NoSQL database service, but "fully managed" doesn't mean you can ignore its operational health. Monitoring DynamoDB means continuously observing its performance metrics, resource utilization, and error patterns to ensure your application runs smoothly, costs stay predictable, and users remain happy. The monitoring ecosystem for DynamoDB consists of three pillars: metrics (the raw numerical data about your tables and indexes), alarms (automated alerting when metrics cross dangerous thresholds), and dashboards (visual representations that give you at-a-glance situational awareness).

DynamoDB automatically sends metrics to Amazon CloudWatch at no additional cost. These metrics are the foundation upon which alarms and dashboards are built. Understanding what each metric means, how to interpret it, and when to act on it is the core skill this tutorial will teach you.

Why DynamoDB Monitoring Matters

Without monitoring, DynamoDB operates as a black box. Your application could be experiencing throttling, high latency, or runaway costs without you knowing until customers complain. Effective monitoring delivers several concrete benefits:

Key DynamoDB Metrics Explained

DynamoDB exposes a rich set of CloudWatch metrics. They fall into several logical categories. Let's explore the most important ones that every developer and operator should know.

Capacity Metrics: Provisioned vs. On-Demand

For tables using provisioned capacity, the critical metrics are:

For tables using on-demand capacity, the equivalents are:

On-demand tables don't have provisioned capacity metrics, but you can still monitor consumed units to understand cost trends, since you pay per request unit.

Throttling and Error Metrics

Latency Metrics

Operational and DAX Metrics

Setting Up CloudWatch Alarms for DynamoDB

Alarms transform passive metrics into active notifications. An alarm watches a single metric over a time window and triggers when the metric breaches a threshold you define. The alarm can then send an SNS notification, trigger an AWS Lambda function for auto-remediation, or simply appear on a dashboard as "in alarm" state.

Creating a Basic Throttling Alarm via AWS CLI

Suppose you have a provisioned table called Orders and you want to be notified when throttled requests exceed 5 per minute, sustained for 5 consecutive minutes. Here's how to create that alarm using the AWS CLI:

aws cloudwatch put-metric-alarm \
  --alarm-name "OrdersTable-Throttling-Alarm" \
  --alarm-description "Alarm when Orders table throttles more than 5 requests/min for 5 minutes" \
  --namespace "AWS/DynamoDB" \
  --metric-name "ThrottledRequests" \
  --dimensions Name=TableName,Value=Orders \
  --statistic "Sum" \
  --period 60 \
  --evaluation-periods 5 \
  --threshold 5 \
  --comparison-operator "GreaterThanOrEqualToThreshold" \
  --treat-missing-data "missing" \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:OpsTeamNotification" \
  --actions-enabled

Let's break down the key parameters:

Creating a High-Latency Alarm for a Specific Operation

For latency, you might want to alarm when the average SuccessfulRequestLatency for Query operations exceeds 100ms over 3 evaluation periods. Notice the additional Operation dimension:

aws cloudwatch put-metric-alarm \
  --alarm-name "OrdersTable-QueryLatency-High" \
  --alarm-description "Alarm when Query latency exceeds 100ms average for 3 minutes" \
  --namespace "AWS/DynamoDB" \
  --metric-name "SuccessfulRequestLatency" \
  --dimensions Name=TableName,Value=Orders Name=Operation,Value=Query \
  --statistic "Average" \
  --period 60 \
  --evaluation-periods 3 \
  --threshold 100 \
  --comparison-operator "GreaterThanThreshold" \
  --treat-missing-data "notBreaching" \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:OpsTeamNotification"

Here, --treat-missing-data "notBreaching" is safer for latency — if the table isn't receiving queries (and thus has no latency data), we don't want to falsely alarm.

Composite Alarms for Complex Conditions

Sometimes you want to alarm only when multiple conditions are true simultaneously. For example: "Alert if throttling is high AND latency is high." CloudWatch supports composite alarms that combine other metric alarms using boolean logic:

aws cloudwatch put-composite-alarm \
  --alarm-name "OrdersTable-Critical-Combined" \
  --alarm-description "Critical: both throttling and high latency detected" \
  --alarm-rule "ALARM(OrdersTable-Throttling-Alarm) AND ALARM(OrdersTable-QueryLatency-High)" \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:PagerDutyIntegration" \
  --actions-enabled

Composite alarms don't have their own metrics, periods, or thresholds — they purely evaluate the state of other alarms. This is powerful for reducing alert noise: you can route individual metric alarms to a low-severity notification channel, and reserve composite alarms for high-severity paging.

Building DynamoDB Dashboards

CloudWatch Dashboards provide a customizable, visual representation of your DynamoDB metrics. A well-designed dashboard lets you spot trends, correlate events, and quickly diagnose issues without sifting through raw metric data.

Creating a Dashboard via the AWS CLI

Below is a complete example that creates a dashboard named DynamoDB-Operations with multiple widgets. The dashboard body is a JSON string containing widget definitions:

aws cloudwatch put-dashboard \
  --dashboard-name "DynamoDB-Operations" \
  --dashboard-body '{
  "widgets": [
    {
      "type": "metric",
      "x": 0,
      "y": 0,
      "width": 12,
      "height": 6,
      "properties": {
        "view": "timeSeries",
        "stacked": false,
        "metrics": [
          ["AWS/DynamoDB", "ConsumedReadCapacityUnits", {"stat": "Sum", "label": "Read RCU Consumed"}],
          ["AWS/DynamoDB", "ProvisionedReadCapacityUnits", {"stat": "Average", "label": "Read RCU Provisioned"}],
          ["AWS/DynamoDB", "ConsumedWriteCapacityUnits", {"stat": "Sum", "label": "Write WCU Consumed"}],
          ["AWS/DynamoDB", "ProvisionedWriteCapacityUnits", {"stat": "Average", "label": "Write WCU Provisioned"}]
        ],
        "region": "us-east-1",
        "title": "Capacity: Consumed vs Provisioned",
        "period": 300
      }
    },
    {
      "type": "metric",
      "x": 12,
      "y": 0,
      "width": 12,
      "height": 6,
      "properties": {
        "view": "timeSeries",
        "stacked": false,
        "metrics": [
          ["AWS/DynamoDB", "ThrottledRequests", {"stat": "Sum", "label": "Throttled Requests"}],
          ["AWS/DynamoDB", "UserErrors", {"stat": "Sum", "label": "User Errors"}],
          ["AWS/DynamoDB", "SystemErrors", {"stat": "Sum", "label": "System Errors"}]
        ],
        "region": "us-east-1",
        "title": "Errors & Throttling",
        "period": 60
      }
    },
    {
      "type": "metric",
      "x": 0,
      "y": 6,
      "width": 8,
      "height": 6,
      "properties": {
        "view": "timeSeries",
        "stacked": false,
        "metrics": [
          ["AWS/DynamoDB", "SuccessfulRequestLatency", "Operation", "GetItem", {"stat": "Average", "label": "GetItem"}],
          ["AWS/DynamoDB", "SuccessfulRequestLatency", "Operation", "PutItem", {"stat": "Average", "label": "PutItem"}],
          ["AWS/DynamoDB", "SuccessfulRequestLatency", "Operation", "Query", {"stat": "Average", "label": "Query"}],
          ["AWS/DynamoDB", "SuccessfulRequestLatency", "Operation", "Scan", {"stat": "Average", "label": "Scan"}]
        ],
        "region": "us-east-1",
        "title": "Latency by Operation (ms)",
        "period": 300
      }
    },
    {
      "type": "metric",
      "x": 8,
      "y": 6,
      "width": 8,
      "height": 6,
      "properties": {
        "view": "singleValue",
        "metrics": [
          ["AWS/DynamoDB", "ConsumedReadCapacityUnits", {"stat": "Sum", "period": 3600, "label": "RCU/Hour"}],
          ["AWS/DynamoDB", "ConsumedWriteCapacityUnits", {"stat": "Sum", "period": 3600, "label": "WCU/Hour"}]
        ],
        "region": "us-east-1",
        "title": "Current Hour Consumption",
        "period": 3600
      }
    },
    {
      "type": "alarm",
      "x": 16,
      "y": 6,
      "width": 8,
      "height": 6,
      "properties": {
        "title": "Active Alarms",
        "alarms": [
          "OrdersTable-Throttling-Alarm",
          "OrdersTable-QueryLatency-High",
          "OrdersTable-Critical-Combined"
        ]
      }
    }
  ]
}'

This dashboard layout uses a 24-column grid. Widgets are positioned by (x, y) coordinates with specified width and height. The widget types include:

Adding a Text Widget with Runbook Information

Including operational context directly on the dashboard reduces mean time to resolution (MTTR) during incidents. Here's how to add a text widget:

{
  "type": "text",
  "x": 0,
  "y": 12,
  "width": 24,
  "height": 4,
  "properties": {
    "markdown": "## Runbook Links\n\n- **Throttling Alert:** [Runbook for Orders table throttling](https://wiki.internal/runbook-orders-throttle)\n- **High Latency Alert:** Check hot partition metrics in CloudWatch Insights\n- **On-call:** #ops-alerts on Slack | PagerDuty escalation: @ops-team\n- **Last updated:** 2025-01-15 by platform-eng"
  }
}

Working with DynamoDB Metrics Programmatically

Beyond the CLI, you'll often need to interact with DynamoDB metrics from application code — for custom dashboards, automated scaling logic, or integration with third-party monitoring systems. Here are practical code examples using the AWS SDK for Python (boto3).

Fetching Metric Data with boto3

This Python script retrieves the ConsumedReadCapacityUnits for a specific table over the last hour and prints the data points:

import boto3
from datetime import datetime, timedelta
import time

# Initialize CloudWatch client
cloudwatch = boto3.client('cloudwatch', region_name='us-east-1')

# Define the time range
end_time = datetime.utcnow()
start_time = end_time - timedelta(hours=1)

response = cloudwatch.get_metric_statistics(
    Namespace='AWS/DynamoDB',
    MetricName='ConsumedReadCapacityUnits',
    Dimensions=[
        {
            'Name': 'TableName',
            'Value': 'Orders'
        }
    ],
    StartTime=start_time,
    EndTime=end_time,
    Period=300,  # 5-minute intervals
    Statistics=['Sum', 'Average', 'Maximum'],
    Unit='Count'
)

# Sort datapoints chronologically
datapoints = sorted(response['Datapoints'], key=lambda x: x['Timestamp'])

print(f"Retrieved {len(datapoints)} datapoints for the last hour:\n")
for dp in datapoints:
    ts = dp['Timestamp'].strftime('%H:%M:%S')
    print(f"  [{ts}] Sum: {dp['Sum']:.1f} | Avg: {dp['Average']:.1f} | Max: {dp['Maximum']:.1f}")

# Calculate total RCU consumed in the period
total_rcu = sum(dp['Sum'] for dp in datapoints)
print(f"\nTotal RCU consumed in last hour: {total_rcu:.1f}")

Checking Alarm State Programmatically

You might want your application to check alarm states before performing critical operations, or to build a health-check endpoint:

import boto3

cloudwatch = boto3.client('cloudwatch', region_name='us-east-1')

# List all alarms for DynamoDB metrics
response = cloudwatch.describe_alarms(
    AlarmNamePrefix='OrdersTable',
    StateValue='ALARM',  # Only retrieve alarms currently in ALARM state
    MaxRecords=50
)

alarming = response.get('MetricAlarms', []) + response.get('CompositeAlarms', [])

if alarming:
    print("CRITICAL: The following alarms are in ALARM state:")
    for alarm in alarming:
        print(f"  - {alarm['AlarmName']}: {alarm.get('StateReason', 'No reason available')}")
else:
    print("All clear — no OrdersTable alarms are currently firing.")

# Get detailed history for a specific alarm
history = cloudwatch.describe_alarm_history(
    AlarmName='OrdersTable-Throttling-Alarm',
    HistoryItemType='StateUpdate',
    StartDate=datetime.utcnow() - timedelta(days=1),
    EndDate=datetime.utcnow(),
    MaxRecords=10
)

print("\nRecent state changes for throttling alarm:")
for item in history.get('AlarmHistoryItems', []):
    ts = item['Timestamp'].strftime('%Y-%m-%d %H:%M:%S')
    summary = item['HistorySummary']
    print(f"  [{ts}] {summary}")

Automating Alarm Creation Across Multiple Tables

If you manage dozens of DynamoDB tables, manually creating alarms for each is tedious and error-prone. This script iterates through all tables and creates a standardized throttling alarm for each:

import boto3

dynamodb = boto3.client('dynamodb', region_name='us-east-1')
cloudwatch = boto3.client('cloudwatch', region_name='us-east-1')

SNS_TOPIC_ARN = 'arn:aws:sns:us-east-1:123456789012:OpsTeamNotification'

# Fetch all table names
tables = []
paginator = dynamodb.get_paginator('list_tables')
for page in paginator.paginate():
    tables.extend(page.get('TableNames', []))

print(f"Found {len(tables)} tables. Creating throttling alarms...")

for table_name in tables:
    alarm_name = f"{table_name}-Throttling-Alarm"
    
    try:
        cloudwatch.put_metric_alarm(
            AlarmName=alarm_name,
            AlarmDescription=f"Auto-generated throttling alarm for {table_name}",
            Namespace='AWS/DynamoDB',
            MetricName='ThrottledRequests',
            Dimensions=[{'Name': 'TableName', 'Value': table_name}],
            Statistic='Sum',
            Period=60,
            EvaluationPeriods=5,
            Threshold=5,
            ComparisonOperator='GreaterThanOrEqualToThreshold',
            TreatMissingData='missing',
            AlarmActions=[SNS_TOPIC_ARGS],
            ActionsEnabled=True
        )
        print(f"  ✓ Created alarm for {table_name}")
    except Exception as e:
        print(f"  ✗ Failed for {table_name}: {str(e)}")

print("Done.")

Infrastructure as Code: CloudFormation Example

For production environments, alarms and dashboards should be defined as infrastructure as code. Here's a CloudFormation snippet that provisions a DynamoDB table along with its monitoring stack:

Resources:
  OrdersTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: Orders
      BillingMode: PROVISIONED
      ProvisionedThroughput:
        ReadCapacityUnits: 10
        WriteCapacityUnits: 10
      AttributeDefinitions:
        - AttributeName: orderId
          AttributeType: S
      KeySchema:
        - AttributeName: orderId
          KeyType: HASH

  OrdersThrottleAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: Orders-Throttling-Alarm
      AlarmDescription: "Alert on throttled requests for Orders table"
      Namespace: AWS/DynamoDB
      MetricName: ThrottledRequests
      Dimensions:
        - Name: TableName
          Value: !Ref OrdersTable
      Statistic: Sum
      Period: 60
      EvaluationPeriods: 5
      Threshold: 5
      ComparisonOperator: GreaterThanOrEqualToThreshold
      TreatMissingData: missing
      AlarmActions:
        - !Ref OpsNotificationTopic

  OrdersLatencyAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: Orders-QueryLatency-Alarm
      AlarmDescription: "Alert when Query latency exceeds 100ms"
      Namespace: AWS/DynamoDB
      MetricName: SuccessfulRequestLatency
      Dimensions:
        - Name: TableName
          Value: !Ref OrdersTable
        - Name: Operation
          Value: Query
      Statistic: Average
      Period: 60
      EvaluationPeriods: 3
      Threshold: 100
      ComparisonOperator: GreaterThanThreshold
      TreatMissingData: notBreaching
      AlarmActions:
        - !Ref OpsNotificationTopic

  OpsNotificationTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: DynamoDB-Ops-Notifications
      Subscription:
        - Endpoint: ops-team@example.com
          Protocol: email

  DynamoDBDashboard:
    Type: AWS::CloudWatch::Dashboard
    Properties:
      DashboardName: DynamoDB-Operations
      DashboardBody: !Sub |
        {
          "widgets": [
            {
              "type": "metric",
              "x": 0, "y": 0, "width": 12, "height": 6,
              "properties": {
                "view": "timeSeries",
                "stacked": false,
                "metrics": [
                  ["AWS/DynamoDB", "ThrottledRequests", "TableName", "${OrdersTable}", {"stat": "Sum"}],
                  ["AWS/DynamoDB", "UserErrors", "TableName", "${OrdersTable}", {"stat": "Sum"}],
                  ["AWS/DynamoDB", "SystemErrors", "TableName", "${OrdersTable}", {"stat": "Sum"}]
                ],
                "region": "${AWS::Region}",
                "title": "Errors & Throttling - Orders Table",
                "period": 60
              }
            },
            {
              "type": "alarm",
              "x": 12, "y": 0, "width": 12, "height": 6,
              "properties": {
                "title": "Alarm Status",
                "alarms": ["${OrdersThrottleAlarm}", "${OrdersLatencyAlarm}"]
              }
            }
          ]
        }

Using CloudFormation (or Terraform/CDK) ensures your monitoring configuration is version-controlled, repeatable across environments, and won't drift from manual console changes.

Best Practices for DynamoDB Monitoring

Over years of operating DynamoDB at scale, several patterns have emerged that separate effective monitoring from noisy, ignored alerts.

1. Use Multi-Dimensional Alarm Evaluation

Don't alarm on a single datapoint. Always use at least --evaluation-periods 3 (preferably 5) to require sustained breach. Momentary spikes in throttling — such as a brief burst during a deployment — should not page an on-call engineer. Combine this with appropriate --treat-missing-data settings to avoid alarms during quiet periods.

2. Monitor Per-Operation Latency, Not Just Aggregate

The aggregate SuccessfulRequestLatency without the Operation dimension blends all operations together. A slow Scan on an infrequently used secondary index might be hidden by thousands of fast GetItem calls. Always break down latency by operation type (GetItem, PutItem, Query, Scan) to catch hidden performance degradations.

3. Set Alarms on Throttling, Not Just Capacity Utilization

It's tempting to alarm when ConsumedReadCapacityUnits exceeds 80% of provisioned capacity. But DynamoDB's adaptive capacity and burst credits can absorb short spikes above provisioned levels without throttling. Instead, alarm directly on ThrottledRequests — it's the actual user-visible failure signal.

4. Build Tiered Alarm Severities

Create at least three tiers:

5. Dashboard for Humans, Alarms for Machines

Design dashboards to be read by tired engineers at 3 AM. Use clear titles, group related metrics, include text widgets with runbook links, and display alarm states prominently. Use the singleValue widget type for current-hour cost metrics. Avoid cramming too many metrics into one widget — a clean layout saves precious minutes during incidents.

6. Track Cost Metrics Separately

For on-demand tables, create a dashboard panel that sums ConsumedReadCapacityUnits and ConsumedWriteCapacityUnits over hourly or daily windows, then multiply by the per-unit pricing in your head (or use AWS Cost Explorer integration). For provisioned tables, monitor ProvisionedReadCapacityUnits over time — capacity you're paying for but not using is waste.

7. Automate Alarm Creation for All Tables

As your application grows, new tables get added. Use the script shown earlier (or a Lambda function triggered by CloudTrail CreateTable events) to automatically deploy baseline alarms to every new table. A table without alarms is a blind spot waiting to become an outage.

8. Test Your Alarms

Periodically simulate throttling or high latency (e.g., by temporarily lowering provisioned capacity in a dev environment) to verify that alarms fire, notifications reach the right people, and runbook documentation is accurate. An untested alarm chain is a false sense of security.

9. Leverage CloudWatch Metric Math for Derived Insights

CloudWatch supports metric math expressions that compute new time-series from existing metrics. For example, you can calculate the throttling rate as a percentage of total requests:

{
  "metrics": [
    ["AWS/DynamoDB", "ThrottledRequests", {"id": "throttled", "stat": "Sum"}],
    ["AWS/DynamoDB", "ConsumedReadCapacityUnits", {"id": "reads", "stat": "Sum"}],
    ["AWS/DynamoDB", "ConsumedWriteCapacityUnits", {"id": "writes", "stat": "Sum"}],
    [{"expression": "(throttled / (reads + writes)) * 100", "label": "ThrottleRate%", "id": "rate"}]
  ],
  "view": "timeSeries",
  "title": "Throttle Rate as % of Total Ops"
}

This derived metric is often more actionable than raw throttle counts — it normalizes against traffic volume.

10. Use Contributor Insights for Hot Partition Detection

While CloudWatch metrics give aggregate data, DynamoDB Contributor Insights (when enabled) provides per-partition breakdowns. This is invaluable for detecting hot keys — individual partition keys that receive disproportionate traffic and cause throttling despite adequate aggregate capacity. Enable Contributor Insights for production tables and monitor the ContributedThroughput metric with partition dimensions.

Conclusion

Monitoring DynamoDB is not a one-time setup task — it's an ongoing discipline that matures alongside your application. Start with the fundamentals: understand the key metrics, create alarms on throttling and latency, and build a clean dashboard for operational visibility. As your system grows, layer on composite alarms, automated alarm provisioning, metric math, and Contributor Insights. The goal is never to have the most alarms, but to have the right alarms — ones that reliably predict or detect user-impacting issues while minimizing noise. With the patterns and code examples in this tutorial, you have a complete foundation to build a robust, production-grade monitoring system for DynamoDB that keeps your applications fast, your costs visible, and your on-call engineers well-rested.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles