tutorial

Monitoring AWS DynamoDB with Vigilmon: Table Health, Capacity & Application-Layer Availability Checks

Practical guide to uptime and health monitoring for AWS DynamoDB — HTTP health endpoints for DynamoDB-connected services, throttling detection, capacity awareness, and independent availability checks with Vigilmon.

AWS DynamoDB is designed for single-digit millisecond performance at any scale, but your application's availability depends on more than DynamoDB's uptime. Throttled requests, misconfigured capacity, IAM permission errors, and partition key hot-spotting all degrade your application silently while DynamoDB's own availability metrics stay green. CloudWatch can alert on consumed capacity units, but it lives inside your AWS account and can't tell you whether your application can actually perform a read or write. Vigilmon adds an independent external layer: HTTP health checks on your DynamoDB-connected services that probe real table connectivity, detect throttling, and alert on application-level failures.

This tutorial shows you how to build meaningful monitoring for DynamoDB-backed applications using Vigilmon.

What You'll Build

  • A health endpoint that probes real DynamoDB read/write availability
  • A Vigilmon HTTP monitor with appropriate timeout and assertion settings
  • A heartbeat pattern for DynamoDB-backed batch jobs and stream processors
  • A throttling-aware health check that surfaces capacity issues before they cascade
  • An alerting strategy for table unavailability and write throttling

Prerequisites

  • AWS account with at least one DynamoDB table
  • IAM role or user with dynamodb:GetItem, dynamodb:PutItem (or at minimum dynamodb:DescribeTable)
  • An application connected to DynamoDB (any runtime)
  • A free account at vigilmon.online

Step 1: Add a Health Endpoint to Your DynamoDB Service

Add a /health endpoint that performs a lightweight DynamoDB operation to verify real connectivity and capacity availability — not just that the SDK loaded correctly.

Node.js (@aws-sdk/client-dynamodb)

// routes/health.js
import { DynamoDBClient, GetItemCommand, DescribeTableCommand } from '@aws-sdk/client-dynamodb';

const ddb = new DynamoDBClient({
  maxAttempts: 1, // No retries in health checks — fail fast
});

export async function healthHandler(req, res) {
  const checks = {};
  let ok = true;
  const start = Date.now();

  // Check 1: DescribeTable — verifies IAM permissions and table existence
  try {
    const tableInfo = await ddb.send(
      new DescribeTableCommand({ TableName: process.env.DDB_TABLE_NAME })
    );
    const tableStatus = tableInfo.Table.TableStatus;
    checks.tableStatus = tableStatus;

    if (tableStatus !== 'ACTIVE') {
      checks.table = `not active: ${tableStatus}`;
      ok = false;
    } else {
      checks.table = 'ok';
    }

    // Report capacity mode
    const billing = tableInfo.Table.BillingModeSummary?.BillingMode || 'PROVISIONED';
    checks.billingMode = billing;

    if (billing === 'PROVISIONED') {
      const readCapacity = tableInfo.Table.ProvisionedThroughput.ReadCapacityUnits;
      const writeCapacity = tableInfo.Table.ProvisionedThroughput.WriteCapacityUnits;
      checks.readCapacityUnits = readCapacity;
      checks.writeCapacityUnits = writeCapacity;
    }
  } catch (err) {
    checks.table = `error: ${err.name} - ${err.message}`;
    ok = false;
  }

  // Check 2: GetItem probe (optional, uses a known probe key)
  if (ok && process.env.DDB_PROBE_KEY) {
    try {
      await ddb.send(
        new GetItemCommand({
          TableName: process.env.DDB_TABLE_NAME,
          Key: { [process.env.DDB_PK_NAME]: { S: process.env.DDB_PROBE_KEY } },
        })
      );
      const latencyMs = Date.now() - start;
      checks.readLatencyMs = latencyMs;
      checks.read = 'ok';

      const warnMs = parseInt(process.env.DDB_LATENCY_WARN_MS || '1000', 10);
      if (latencyMs > warnMs) {
        checks.read = 'slow';
        ok = false;
      }
    } catch (err) {
      if (err.name === 'ProvisionedThroughputExceededException') {
        checks.read = 'throttled';
        ok = false;
      } else {
        checks.read = `error: ${err.name}`;
        ok = false;
      }
    }
  }

  res.status(ok ? 200 : 503).json({
    status: ok ? 'ok' : 'degraded',
    service: 'dynamodb-service',
    checks,
  });
}

Python (boto3)

# routers/health.py
import os, time, boto3
from botocore.exceptions import ClientError
from fastapi import APIRouter

router = APIRouter()
ddb = boto3.client('dynamodb', config=boto3.session.Config(retries={'max_attempts': 1}))

@router.get("/health")
async def health():
    checks = {}
    ok = True
    start = time.monotonic()

    try:
        resp = ddb.describe_table(TableName=os.environ['DDB_TABLE_NAME'])
        table = resp['Table']
        status = table['TableStatus']
        checks['tableStatus'] = status

        if status != 'ACTIVE':
            checks['table'] = f'not active: {status}'
            ok = False
        else:
            checks['table'] = 'ok'

        billing = table.get('BillingModeSummary', {}).get('BillingMode', 'PROVISIONED')
        checks['billingMode'] = billing
    except ClientError as e:
        checks['table'] = f"error: {e.response['Error']['Code']}"
        ok = False

    if ok and os.environ.get('DDB_PROBE_KEY'):
        try:
            ddb.get_item(
                TableName=os.environ['DDB_TABLE_NAME'],
                Key={os.environ['DDB_PK_NAME']: {'S': os.environ['DDB_PROBE_KEY']}},
            )
            latency_ms = int((time.monotonic() - start) * 1000)
            checks['readLatencyMs'] = latency_ms
            checks['read'] = 'ok'
        except ClientError as e:
            code = e.response['Error']['Code']
            checks['read'] = 'throttled' if code == 'ProvisionedThroughputExceededException' else f'error: {code}'
            ok = False

    return {
        'status': 'ok' if ok else 'degraded',
        'service': 'dynamodb-service',
        'checks': checks,
    }

Go (aws-sdk-go-v2)

// internal/health/handler.go
package health

import (
    "context"
    "encoding/json"
    "net/http"
    "os"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/service/dynamodb"
    "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)

func Handler(client *dynamodb.Client) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        checks := map[string]any{}
        ok := true
        start := time.Now()

        ctx, cancel := context.WithTimeout(r.Context(), 8*time.Second)
        defer cancel()

        out, err := client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
            TableName: aws.String(os.Getenv("DDB_TABLE_NAME")),
        })
        if err != nil {
            checks["table"] = "error: " + err.Error()
            ok = false
        } else {
            status := string(out.Table.TableStatus)
            checks["tableStatus"] = status
            if out.Table.TableStatus != types.TableStatusActive {
                checks["table"] = "not active: " + status
                ok = false
            } else {
                checks["table"] = "ok"
                checks["readLatencyMs"] = time.Since(start).Milliseconds()
            }
        }

        w.Header().Set("Content-Type", "application/json")
        if !ok {
            w.WriteHeader(http.StatusServiceUnavailable)
        }
        json.NewEncoder(w).Encode(map[string]any{
            "status":  map[bool]string{true: "ok", false: "degraded"}[ok],
            "service": "dynamodb-service",
            "checks":  checks,
        })
    }
}

Step 2: Configure the Vigilmon HTTP Monitor

  1. Log in to VigilmonAdd Monitor → HTTP.
  2. URL: your service's health endpoint (e.g. https://api.example.com/health).
  3. Check interval: 60 seconds.
  4. Response timeout: 10 seconds.
  5. Expected status: 200.
  6. JSON assertion: path status, expected value ok.

Tip: Set DDB_PROBE_KEY to a dedicated "health-check" item in your table (with a known partition key) so your health endpoint exercises the full read path without touching real data. Write a sentinel item once during provisioning and never delete it.


Step 3: Heartbeat Monitoring for DynamoDB Stream Processors

DynamoDB Streams feed Lambda functions and Kinesis Data Streams consumers that react to table changes. These processors are invisible to HTTP monitors. Use Vigilmon's Heartbeat monitor and ping it after each successful batch of stream records.

Lambda DynamoDB Streams heartbeat

// lambda/stream-processor.mjs
export async function handler(event) {
  let processed = 0;

  for (const record of event.Records) {
    if (record.eventName === 'INSERT' || record.eventName === 'MODIFY') {
      await processRecord(record.dynamodb.NewImage);
      processed++;
    }
  }

  // Ping heartbeat after each successful batch invocation
  if (processed > 0) {
    await fetch(process.env.VIGILMON_HEARTBEAT_URL, { method: 'POST' });
    console.log(`Processed ${processed} stream records — heartbeat sent`);
  }

  return { statusCode: 200, processed };
}

In Vigilmon, create a Heartbeat monitor:

  • Set the grace period to 5× the maximum time between expected table writes.
  • If your table is written to every 30 seconds, set the grace period to 3 minutes.
  • A missed heartbeat means your Lambda failed, the DynamoDB Stream shard iterator expired, or table writes stopped unexpectedly.

Step 4: Throttling Detection

DynamoDB throttling is a silent performance killer. When provisioned capacity runs out or on-demand capacity is exhausted, write and read requests fail with ProvisionedThroughputExceededException. Catch this explicitly in your health check:

// Separate write-path health check
export async function writeHealthHandler(req, res) {
  const checks = {};
  let ok = true;

  try {
    // Use a dedicated health-check item — write a timestamp
    await ddb.send(
      new PutItemCommand({
        TableName: process.env.DDB_TABLE_NAME,
        Item: {
          [process.env.DDB_PK_NAME]: { S: '__health_probe__' },
          lastCheck: { S: new Date().toISOString() },
          ttl: { N: String(Math.floor(Date.now() / 1000) + 3600) }, // Auto-delete in 1 hour
        },
        ConditionExpression: 'attribute_exists(#pk) OR attribute_not_exists(#pk)',
        ExpressionAttributeNames: { '#pk': process.env.DDB_PK_NAME },
      })
    );
    checks.write = 'ok';
  } catch (err) {
    if (err.name === 'ProvisionedThroughputExceededException') {
      checks.write = 'throttled — capacity exhausted';
    } else {
      checks.write = `error: ${err.name}`;
    }
    ok = false;
  }

  res.status(ok ? 200 : 503).json({ status: ok ? 'ok' : 'degraded', checks });
}

Configure this as a second Vigilmon monitor on /health/write to independently track read and write path availability.


Step 5: Alerting Strategy

| Alert channel | When to use | |---|---| | Email / PagerDuty | Service returns 503 (table unavailable or throttled) | | Slack webhook | Heartbeat missed (stream processor stalled) | | Slack + Email | Write throttling detected (capacity exhausted) | | Status page | Public API backed by DynamoDB tables |

Configure alert escalation in Vigilmon: notify immediately on first failure, re-notify after 5 minutes. DynamoDB throttling can cascade quickly through an application.


What Vigilmon Catches That CloudWatch Misses

| Scenario | CloudWatch | Vigilmon | |---|---|---| | Application can't connect (IAM error) | No built-in application-layer check | HTTP monitor catches 503 immediately | | Table in CREATING or UPDATING state | CloudWatch shows table exists | Health endpoint detects non-ACTIVE status | | Stream processor silently fails | Needs Lambda error alarm + SNS | Heartbeat grace period fires alert | | Write path throttled, read path fine | Separate capacity unit metrics | Write health endpoint catches independently | | CloudWatch alarm SNS topic broken | Alerts are silently lost | Vigilmon is external — unaffected | | Region-wide DynamoDB degradation | AWS Status Page (reactive) | HTTP monitor fires independently in real time |


DynamoDB handles massive scale reliably, but throttling, stale capacity settings, and stream processing failures can quietly break your application. External health checks from Vigilmon give you the independent signal you need — before your users report errors.

Start monitoring your DynamoDB applications in under 5 minutes — register free at vigilmon.online.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →