Guide · AWS Backup · Restore Testing · Validation · Disaster Recovery
AWS Backup Restore Testing — Automated Validation of MCP Server Recovery Points
A backup you have never restored is an assumption, not a guarantee. AWS Backup Restore Testing automates the cycle: periodically restore a recovery point into a temporary resource, run a validation Lambda against the restored resource, report success or failure, then clean up — without manual intervention. This gives you continuous assurance that your MCP server's DynamoDB tables, EFS volumes, and RDS databases can actually be recovered, not just that the backup job ran without errors. This guide covers how to create a RestoreTestingPlan, wire a RestoreTestingSelection to pick the right recovery points, write a validation Lambda that checks data integrity (row count, schema shape, write test), and configure the validation window so cleanup doesn't delete the restored resource before validation completes.
TL;DR
Create a restore testing plan with scheduleExpression: cron(0 6 ? * SUN *) (weekly Sunday at 6am UTC). Select resources by tag RestoreTesting=true. Set restoreTestingRecoveryPointSelection to LATEST_WITHIN_WINDOW with a 24-hour window so you always test yesterday's backup. Set the validation window to 4 hours — if your Lambda doesn't report success within that window, the test is marked failed. Write a validation Lambda that queries the restored DynamoDB table's row count, compares it against the last-known production row count stored in SSM Parameter Store, and calls backup:PutRestoreValidationResult with SUCCESSFUL or FAILED.
Restore testing lifecycle
AWS Backup Restore Testing runs the following sequence automatically:
- Recovery point selection: picks a recovery point matching the selection criteria (latest within N days, or random within window)
- Restore: starts a restore job using the same metadata you specify (restore parameters, target resource name pattern)
- Validation window opens: once restore completes, AWS Backup invokes your validation Lambda (if configured) and starts a countdown timer (your
restoreTestingPlanValidationWindowHours) - Validation Lambda runs: checks data integrity, calls
backup:PutRestoreValidationResult - Cleanup: after the validation window closes (or after Lambda reports a result), AWS Backup deletes the restored resource
If the validation window expires without a PutRestoreValidationResult call, AWS Backup marks the test TIMED_OUT — not failed. Use a timeout that is comfortably longer than your expected Lambda execution time plus any retry backoff.
Creating a restore testing plan
import * as backup from 'aws-cdk-lib/aws-backup';
// AWS Backup Restore Testing is not yet in CDK L2 as of late 2026 — use L1 (CfnRestoreTestingPlan)
import { CfnRestoreTestingPlan, CfnRestoreTestingSelection } from 'aws-cdk-lib/aws-backup';
const restoreTestingPlan = new CfnRestoreTestingPlan(this, 'McpRestoreTestingPlan', {
restoreTestingPlanName: 'alivemcp-weekly-restore-test',
scheduleExpression: 'cron(0 6 ? * SUN *)', // weekly Sunday 06:00 UTC
scheduleExpressionTimezone: 'UTC',
startWindowHours: 1, // must start within 1 hour of schedule time
recoveryPointSelection: {
algorithm: 'LATEST_WITHIN_WINDOW',
includeVaults: [
`arn:aws:backup:us-east-1:${this.account}:backup-vault:alivemcp-prod`,
],
recoveryPointTypes: ['SNAPSHOT'],
selectionWindowDays: 1, // only consider recovery points from the last 24 hours
},
});
Recovery point selection algorithms
| Algorithm | Behavior | Best for |
|---|---|---|
LATEST_WITHIN_WINDOW | Selects the most recent recovery point within selectionWindowDays | Standard testing — always test your most recent backup |
RANDOM_WITHIN_WINDOW | Selects a random recovery point within the window | Long-term validation — periodically verify older recovery points are still restorable |
Creating a restore testing selection
The RestoreTestingSelection links the plan to specific resources and specifies the restore parameters (table name pattern, IAM role) and the validation Lambda ARN.
const restoreTestingSelection = new CfnRestoreTestingSelection(this, 'DynamoRestoreSelection', {
restoreTestingPlanName: restoreTestingPlan.restoreTestingPlanName,
restoreTestingSelectionName: 'dynamodb-alivemcp-sessions',
iamRoleArn: `arn:aws:iam::${this.account}:role/AWSBackupDefaultServiceRole`,
protectedResourceType: 'DynamoDB',
protectedResourceConditions: {
stringEquals: [
{ key: 'aws:ResourceTag/RestoreTesting', value: 'true' },
],
},
// Restore parameters for DynamoDB
restoreMetadataOverrides: {
// The restored table will be named {originalName}-restore-test-{timestamp}
targetTableName: 'alivemcp-sessions-restore-test',
// billingMode and other properties are inherited from the recovery point
},
validationWindowHours: 4,
// Lambda ARN to invoke after restore completes
// The Lambda receives a RestoreTestingJobId it must use in PutRestoreValidationResult
// (Validation Lambda ARN is not a direct property of CfnRestoreTestingSelection
// as of CDK — wire it via the plan's EventBridge notification instead)
});
Note: as of late 2026, the validation Lambda is wired through the restore testing plan's EventBridge events rather than directly on the selection. When the restore job status changes to COMPLETED, AWS Backup emits a Restore Testing Job State Change event that your Lambda can subscribe to.
Writing the validation Lambda
The validation Lambda receives the restore testing job details via EventBridge, queries the restored resource, and calls backup:PutRestoreValidationResult. Here is a complete implementation for DynamoDB row count validation:
// validation-lambda/index.ts
import { DynamoDBClient, DescribeTableCommand, ScanCommand } from '@aws-sdk/client-dynamodb';
import { BackupClient, PutRestoreValidationResultCommand } from '@aws-sdk/client-backup';
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
const dynamo = new DynamoDBClient({});
const backupClient = new BackupClient({});
const ssm = new SSMClient({});
export const handler = async (event: AWSLambda.EventBridgeEvent) => {
const { restoreTestingJobId, createdResourceArn } = event.detail;
// createdResourceArn is the ARN of the restored DynamoDB table
const tableName = createdResourceArn.split('/').pop()!;
let validationStatus: 'SUCCESSFUL' | 'FAILED' = 'FAILED';
let validationStatusMessage = '';
try {
// Check 1: table is in ACTIVE state
const describeResult = await dynamo.send(new DescribeTableCommand({ TableName: tableName }));
const tableStatus = describeResult.Table?.TableStatus;
if (tableStatus !== 'ACTIVE') {
throw new Error(`Table status is ${tableStatus}, expected ACTIVE`);
}
// Check 2: row count within 10% of expected production count
const { Parameter } = await ssm.send(new GetParameterCommand({
Name: '/alivemcp/sessions-table-row-count',
}));
const expectedCount = parseInt(Parameter?.Value ?? '0', 10);
// For large tables, use DescribeTable ItemCount (approximate, updated every 6h)
const actualCount = describeResult.Table?.ItemCount ?? 0;
const delta = Math.abs(actualCount - expectedCount) / (expectedCount || 1);
if (delta > 0.1) {
throw new Error(
`Row count mismatch: expected ~${expectedCount}, got ${actualCount} (${(delta * 100).toFixed(1)}% delta > 10% threshold)`
);
}
// Check 3: perform a test write and delete to verify table is writable
const testKey = { sessionId: { S: `restore-test-${Date.now()}` } };
await dynamo.send(new (await import('@aws-sdk/client-dynamodb')).PutItemCommand({
TableName: tableName,
Item: { ...testKey, createdAt: { S: new Date().toISOString() }, testWrite: { BOOL: true } },
}));
await dynamo.send(new (await import('@aws-sdk/client-dynamodb')).DeleteItemCommand({
TableName: tableName,
Key: testKey,
}));
validationStatus = 'SUCCESSFUL';
validationStatusMessage = `Table ${tableName} is ACTIVE with ~${actualCount} items (expected ~${expectedCount}); write test passed`;
} catch (err) {
validationStatusMessage = (err as Error).message;
}
// Report result — must be called within the validation window
await backupClient.send(new PutRestoreValidationResultCommand({
RestoreJobId: restoreTestingJobId,
ValidationStatus: validationStatus,
ValidationStatusMessage: validationStatusMessage,
}));
console.log({ restoreTestingJobId, validationStatus, validationStatusMessage });
};
Wire the Lambda to the EventBridge rule for restore testing job state changes:
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
new events.Rule(this, 'RestoreTestingCompletedRule', {
eventPattern: {
source: ['aws.backup'],
detailType: ['Restore Testing Job State Change'],
detail: {
status: ['COMPLETED'],
restoreTestingPlanName: ['alivemcp-weekly-restore-test'],
},
},
targets: [new targets.LambdaFunction(validationLambda)],
});
// Grant the Lambda permission to call PutRestoreValidationResult
validationLambda.addToRolePolicy(new iam.PolicyStatement({
actions: ['backup:PutRestoreValidationResult'],
resources: ['*'],
}));
// Grant read on SSM parameter for expected row count
validationLambda.addToRolePolicy(new iam.PolicyStatement({
actions: ['ssm:GetParameter'],
resources: [`arn:aws:ssm:${this.region}:${this.account}:parameter/alivemcp/*`],
}));
// Grant DynamoDB access for the restored table
validationLambda.addToRolePolicy(new iam.PolicyStatement({
actions: ['dynamodb:DescribeTable', 'dynamodb:PutItem', 'dynamodb:DeleteItem'],
resources: [`arn:aws:dynamodb:${this.region}:${this.account}:table/alivemcp-sessions-restore-test*`],
}));
Keeping production row count in SSM
The validation Lambda compares restored row count against an expected value stored in SSM Parameter Store. Update this parameter nightly from a lightweight Lambda or scheduled query:
// nightly-row-count-updater/index.ts
import { DynamoDBClient, DescribeTableCommand } from '@aws-sdk/client-dynamodb';
import { SSMClient, PutParameterCommand } from '@aws-sdk/client-ssm';
const dynamo = new DynamoDBClient({});
const ssm = new SSMClient({});
export const handler = async () => {
const result = await dynamo.send(new DescribeTableCommand({
TableName: 'alivemcp-sessions',
}));
const count = result.Table?.ItemCount ?? 0;
await ssm.send(new PutParameterCommand({
Name: '/alivemcp/sessions-table-row-count',
Value: String(count),
Type: 'String',
Overwrite: true,
}));
console.log(`Updated expected row count to ${count}`);
};
DynamoDB's ItemCount from DescribeTable is approximate (updated every 6 hours). For exact counts, run a SELECT count(*) FROM "alivemcp-sessions" via PartiQL — but be aware this does a full table scan and will consume read capacity. For restore testing purposes, the approximate count with a 10% tolerance window is sufficient.
Viewing restore testing results
AWS Backup publishes restore testing job results to the console under Backup → Restore testing and also via the API:
# List restore testing jobs for the plan
aws backup list-restore-testing-inferences \
--restore-testing-plan-name alivemcp-weekly-restore-test \
--query 'RestoreTestingInferences[*].{JobId:RestoreTestingJobId,Status:Status,ValidationStatus:ValidationStatus}'
# Describe a specific job for details
aws backup get-restore-testing-inference \
--restore-testing-plan-name alivemcp-weekly-restore-test \
--restore-testing-job-id JOB_ID
Common failures
| Symptom | Cause | Fix |
|---|---|---|
Restore testing job status TIMED_OUT | Validation Lambda did not call PutRestoreValidationResult within the validation window | Increase validationWindowHours; check Lambda CloudWatch logs for errors; ensure the EventBridge rule is correctly routing restore testing events to the Lambda |
| Restore testing plan has no eligible recovery points | No recovery points exist in the specified vaults within selectionWindowDays | Verify the backup plan ran successfully in the last N days; check the vault name and account match; check backup plan tags align with resource tags |
| Restored table has fewer rows than expected | Recovery point was taken during a low-traffic period; or restore captured an on-demand snapshot before bulk load completed | Adjust row count threshold; consider checking for minimum absolute count rather than percentage delta for tables with high variance |
| Validation Lambda can't describe the restored table | Lambda IAM role doesn't have permission on the dynamically-named restored table | Use a resource ARN with * suffix: arn:aws:dynamodb:region:account:table/alivemcp-sessions-restore-test* |
| Restore testing cleanup deleted the table before validation Lambda ran | Validation window is shorter than the Lambda's initialization time plus restore job processing lag | Increase validationWindowHours to at least 2 hours; cold-start Lambda initialization plus DynamoDB table restore can take 20–40 minutes for medium tables |