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.

PropertyPITR behaviorOn-demand backup comparison
Backup triggerAutomatic and continuous — always enabledManual API call (CreateBackup) or EventBridge Scheduler trigger
Retention windowRolling 35 days (configurable to 1-35 days)Retained indefinitely until explicitly deleted
Restore granularityAny second within the 35-day windowOnly the exact moment the backup was taken
Restore targetNew table (cannot restore over existing table)New table
Throughput during backupZero impact on table RCU/WCUZero impact during snapshot
Cost$0.20/GB/month on top of base storage$0.10/GB/month (first 50 GB free)
GSI restorationAll GSIs restored with the table; must not conflict with existing table namesAll 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:

FormatAdvantageDisadvantageBest for
DYNAMODB_JSONHuman-readable; Athena SerDe available; familiar to DynamoDB developersLarger file size than ION; type descriptors ({"S":"value"}) require SerDe or parsing in LambdaAthena queries, Lambda processing, human inspection
ION~30% smaller than DynamoDB JSON; type-aware binary encodingRequires ION library for reading; less tooling supportLarge 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:

ConstraintValueImplication
Minimum window size15 minutesCannot capture changes in windows shorter than 15 minutes
Maximum window size24 hoursFor continuous history, chain multiple exports to cover longer periods
Minimum gap between exports15 minutesCannot run overlapping incremental exports; schedule at 1-hour intervals for safety
PITR must be enabledRequiredPITR is the underlying mechanism for incremental export; disabling PITR breaks incremental exports
ExportViewType optionsNEW_IMAGE or NEW_AND_OLD_IMAGESNEW_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 componentCalculationMonthly cost
Base table storage (100k items × 1 KB)100 MB × $0.25/GB$0.025
PITR overhead100 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

SymptomRoot causeDetectionFix
ExportTableToPointInTime fails with ValidationExceptionRequested ExportTime is before PITR was enabled — cannot export data from before PITR activationError 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 filesExport window contains no item changes — no writes to the table in the specified windowmanifest-files.json shows itemCount: 0 for all data filesExpected 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 hoursLarge table restore; DynamoDB copies all data including indexes — time scales with data sizeDescribeTable shows TableStatus=CREATING with no error; no CloudWatch RestoreComplete event yetNormal 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 AthenaAthena shows HIVE_BAD_DATA error on first query; first record shows nested S/N type structureUse 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 missingMinimum 15-minute window constraint — export from 14:00 to 14:55 misses the 14:55-15:00 window if next export starts at 15:00Time-series analysis shows gap in incremental export coverage; items modified at 14:57 not in any exportOverlap 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