Guide · DynamoDB Advanced
DynamoDB PITR and S3 Export for MCP Servers — Point-in-Time Recovery, Incremental Export, Athena
DynamoDB's Point-in-Time Recovery (PITR) and ExportTableToPointInTime API together give MCP monitoring platforms two distinct but complementary capabilities. PITR answers the disaster recovery question: if a bug or incident corrupts the probe state table — malicious deletion of server records, accidental overwrite of subscription metadata, or a bad migration — you can restore the table to any second within the past 35 days. ExportTableToPointInTime answers the analytics question: you need month-end SLA reports for each monitored MCP server, but running analytical queries against the operational DynamoDB table consumes read capacity, competes with probe writers, and doesn't support Athena SQL. The S3 export creates a cost-isolated snapshot that Athena can query with standard SQL without touching the production table. Together, PITR and S3 exports cover the operational and analytical needs of a probe state table that grows to millions of records per month.
TL;DR
Enable PITR on your probe state table (adds $0.20/GB/month, no change to table operations). For monthly SLA reports, trigger ExportTableToPointInTime at month-end — the export runs against a consistent snapshot with zero impact on read/write capacity. For continuous probe history in S3, use incremental exports with a 1-hour window; each export captures all item modifications in that window. Athena can query exports with partition projection using the S3 export manifest. Restore from PITR by specifying an ISO 8601 timestamp — the restored table is created alongside the original (never overwrites in-place).
PITR mechanics and what it protects against
PITR maintains a rolling 35-day window of continuous backups. Every item written to the table is captured at per-item granularity, not at transaction granularity — this means you can restore to any second in the 35-day window, not just to the exact timestamps of explicit backup operations.
| Property | PITR behavior | On-demand backup comparison |
|---|---|---|
| Backup trigger | Automatic and continuous — always enabled | Manual API call (CreateBackup) or EventBridge Scheduler trigger |
| Retention window | Rolling 35 days (configurable to 1-35 days) | Retained indefinitely until explicitly deleted |
| Restore granularity | Any second within the 35-day window | Only the exact moment the backup was taken |
| Restore target | New table (cannot restore over existing table) | New table |
| Throughput during backup | Zero impact on table RCU/WCU | Zero impact during snapshot |
| Cost | $0.20/GB/month on top of base storage | $0.10/GB/month (first 50 GB free) |
| GSI restoration | All GSIs restored with the table; must not conflict with existing table names | All GSIs restored |
PITR protects against application-layer corruption: a probe runner bug that writes incorrect status to all server records, an IAM policy that accidentally granted REMOVE permissions to a Lambda function that deleted active server registrations, or a migration script that overwrote subscription metadata with test values. It does not protect against malicious actor with root access or AWS account-level events — those require AWS Backup with cross-account or cross-region copies.
# Restore to 24 hours ago (moment before a bad migration ran)
aws dynamodb restore-table-to-point-in-time \
--source-table-name alivemcp-probe-state \
--target-table-name alivemcp-probe-state-restored-20261031 \
--restore-date-time "2026-10-30T14:00:00Z" \
--billing-mode-override PAY_PER_REQUEST \
--region us-east-1
# Note: target table must not exist
# Provisioned capacity can be overridden via --provisioned-throughput-override
# SSE settings can be overridden via --sse-specification-override
# Restore time scales with table size — small tables (<1 GB): minutes; large tables (>100 GB): hours
ExportTableToPointInTime for S3 archive
The export API creates a consistent snapshot of the table (or a subset via a filter expression) at a specific point in time and writes it to S3. Unlike PITR restore (which creates a new DynamoDB table), ExportTableToPointInTime writes DynamoDB JSON or ION-formatted records to S3 for downstream processing.
// CDK: scheduled monthly export for SLA report generation
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
// EventBridge Scheduler: run at midnight on the first of each month
const monthlyExportRule = new events.Rule(this, 'MonthlyDynamoExport', {
schedule: events.Schedule.cron({
minute: '0',
hour: '0',
day: '1',
month: '*',
year: '*'
})
});
monthlyExportRule.addTarget(new targets.LambdaFunction(exportTriggerFn));
// Lambda: trigger the export via SDK
import { DynamoDB } from '@aws-sdk/client-dynamodb';
const dynamodb = new DynamoDB({ region: 'us-east-1' });
export const handler = async () => {
const now = new Date();
const exportPoint = new Date(now.getFullYear(), now.getMonth(), 1); // First of current month
const result = await dynamodb.exportTableToPointInTime({
TableArn: process.env.TABLE_ARN!,
ExportTime: exportPoint,
S3Bucket: process.env.EXPORT_BUCKET!,
S3Prefix: `exports/monthly/${exportPoint.getFullYear()}-${String(exportPoint.getMonth() + 1).padStart(2, '0')}/`,
ExportFormat: 'DYNAMODB_JSON', // DynamoDB JSON (type descriptors) or ION (binary)
// S3SseAlgorithm: 'AES256', // or 'KMS' with S3SseKmsKeyId
ExportType: 'FULL_EXPORT' // or 'INCREMENTAL_EXPORT' with ExportFromTime/ExportToTime
});
console.log('Export ARN:', result.ExportDescription?.ExportArn);
console.log('Export status:', result.ExportDescription?.ExportStatus); // PENDING initially
// Export runs asynchronously; poll DescribeExport or receive EventBridge notification on completion
};
Export format comparison:
| Format | Advantage | Disadvantage | Best for |
|---|---|---|---|
| DYNAMODB_JSON | Human-readable; Athena SerDe available; familiar to DynamoDB developers | Larger file size than ION; type descriptors ({"S":"value"}) require SerDe or parsing in Lambda | Athena queries, Lambda processing, human inspection |
| ION | ~30% smaller than DynamoDB JSON; type-aware binary encoding | Requires ION library for reading; less tooling support | Large tables where storage cost and export time matter; Spark/EMR processing |
Incremental export for continuous probe history
Incremental export (ExportType: INCREMENTAL_EXPORT) captures only the items that were created, modified, or deleted within a specified time window. This enables CDC-style probe history in S3 without continuously streaming from DynamoDB Streams to a separate pipeline.
// Incremental export: capture all probe state changes in the past hour
const hourAgo = new Date(Date.now() - 3600 * 1000);
const now = new Date();
await dynamodb.exportTableToPointInTime({
TableArn: process.env.TABLE_ARN!,
ExportFormat: 'DYNAMODB_JSON',
ExportType: 'INCREMENTAL_EXPORT',
IncrementalExportSpecification: {
ExportFromTime: hourAgo, // Inclusive start of the window
ExportToTime: now, // Exclusive end of the window
ExportViewType: 'NEW_AND_OLD_IMAGES' // Include both before/after state for each changed item
// ExportViewType: 'NEW_IMAGE' // Only include the final state of changed items
},
S3Bucket: process.env.EXPORT_BUCKET!,
S3Prefix: `exports/incremental/${now.toISOString().slice(0, 13)}/` // Hourly prefix
});
Incremental export constraints:
| Constraint | Value | Implication |
|---|---|---|
| Minimum window size | 15 minutes | Cannot capture changes in windows shorter than 15 minutes |
| Maximum window size | 24 hours | For continuous history, chain multiple exports to cover longer periods |
| Minimum gap between exports | 15 minutes | Cannot run overlapping incremental exports; schedule at 1-hour intervals for safety |
| PITR must be enabled | Required | PITR is the underlying mechanism for incremental export; disabling PITR breaks incremental exports |
| ExportViewType options | NEW_IMAGE or NEW_AND_OLD_IMAGES | NEW_AND_OLD_IMAGES ~2× larger but enables delta detection in downstream Athena queries |
S3 export structure and Athena integration
DynamoDB exports to S3 using a consistent folder structure. Each export produces a manifest file and data files:
# S3 folder structure for a DynamoDB JSON export:
s3://alivemcp-exports/exports/monthly/2026-10/
manifest-summary.json # Export metadata (table ARN, export time, item count, etc.)
manifest-files.json # List of data files with row counts
AWSDynamoDB/
01JD1EXAMPLE2XAMPLE3XAMPLE4/ # Export ID folder
data/
kn96rkrfqbpx.json.gz # Compressed NDJSON of DynamoDB records
l0e2tpmr3cqx.json.gz # Multiple data files for large tables
_started
_finished
-- Athena DDL: query DynamoDB JSON export with partition projection
-- Requires aws-glue-hive-serde (Amazon S3 Select for DynamoDB JSON)
CREATE EXTERNAL TABLE probe_state_archive (
Item struct<
PK: struct<S: string>,
SK: struct<S: string>,
status: struct<S: string>,
latencyMs: struct<N: string>,
lastCheckedAt: struct<N: string>,
consecutiveFailures: struct<N: string>
>
)
ROW FORMAT SERDE 'com.amazonaws.glue.serde.GrokSerDe'
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://alivemcp-exports/exports/monthly/'
TBLPROPERTIES (
'classification'='json',
'compressionType'='gzip'
);
-- Query: find all MCP servers with >1% downtime in October 2026
SELECT
Item.PK.S AS server_slug,
COUNT(*) AS total_probes,
SUM(CASE WHEN Item.status.S != 'HEALTHY' THEN 1 ELSE 0 END) AS failed_probes,
100.0 * SUM(CASE WHEN Item.status.S != 'HEALTHY' THEN 1 ELSE 0 END) / COUNT(*) AS downtime_pct
FROM probe_state_archive
WHERE Item.SK.S = 'STATUS'
GROUP BY Item.PK.S
HAVING downtime_pct > 1.0
ORDER BY downtime_pct DESC;
Cost modeling for PITR and export
For an AliveMCP probe state table with 100,000 monitored endpoints, roughly 1 KB average item size, 60-second probe interval:
| Cost component | Calculation | Monthly cost |
|---|---|---|
| Base table storage (100k items × 1 KB) | 100 MB × $0.25/GB | $0.025 |
| PITR overhead | 100 MB × $0.20/GB/month | $0.020 |
| Monthly full export (DynamoDB JSON, gzip) | ~30 MB compressed × $0.023/GB S3 | $0.001 |
| Hourly incremental exports (24/day × 30 days = 720 exports) | ~5 MB each × 720 × $0.023/GB | $0.083 |
| Athena queries on export data | $5/TB scanned × ~1 GB/month | $0.005 |
| Export S3 storage (12 monthly full + 720 incremental at 1-year retention) | ~4 GB × $0.023/GB | $0.092 |
For a 100,000-server probe table, the full PITR + export archival system costs roughly $0.23/month — negligible compared to the probe runner compute and DynamoDB write costs, but providing 35-day point-in-time recovery and unlimited Athena analytical history.
Complete failure mode reference
| Symptom | Root cause | Detection | Fix |
|---|---|---|---|
| ExportTableToPointInTime fails with ValidationException | Requested ExportTime is before PITR was enabled — cannot export data from before PITR activation | Error message: "Requested export time is before earliest available restore time" | PITR earliest restore time is shown in DescribeContinuousBackups.EarliestRestorableDateTime; only request exports after that timestamp |
| Incremental export produces empty data files | Export window contains no item changes — no writes to the table in the specified window | manifest-files.json shows itemCount: 0 for all data files | Expected behavior for idle tables; if unexpected, verify probe runner is actually writing to the table during the window |
| PITR restore table in CREATING state for >2 hours | Large table restore; DynamoDB copies all data including indexes — time scales with data size | DescribeTable shows TableStatus=CREATING with no error; no CloudWatch RestoreComplete event yet | Normal for large tables; do not delete and retry — the restore is running; monitor DescribeTable until TableStatus=ACTIVE |
| Athena query fails on DynamoDB JSON export: "unable to parse JSON" | DynamoDB JSON uses nested type descriptors ({"S":"value"}) not standard JSON — requires the DynamoDB Ion SerDe or custom SerDe for Athena | Athena shows HIVE_BAD_DATA error on first query; first record shows nested S/N type structure | Use the aws-glue-hive-serde configured for DynamoDB JSON; or use a Glue ETL job to flatten type descriptors to plain JSON before Athena query |
| Incremental export gap: changes from 14:57-15:00 missing | Minimum 15-minute window constraint — export from 14:00 to 14:55 misses the 14:55-15:00 window if next export starts at 15:00 | Time-series analysis shows gap in incremental export coverage; items modified at 14:57 not in any export | Overlap incremental export windows by 5-10 minutes and deduplicate in downstream processing by item-level timestamp; or use DynamoDB Streams for sub-minute CDC instead of incremental export |