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 minimumdynamodb: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
- Log in to Vigilmon → Add Monitor → HTTP.
- URL: your service's health endpoint (e.g.
https://api.example.com/health). - Check interval: 60 seconds.
- Response timeout: 10 seconds.
- Expected status:
200. - JSON assertion: path
status, expected valueok.
Tip: Set
DDB_PROBE_KEYto 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.