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:

  1. Recovery point selection: picks a recovery point matching the selection criteria (latest within N days, or random within window)
  2. Restore: starts a restore job using the same metadata you specify (restore parameters, target resource name pattern)
  3. Validation window opens: once restore completes, AWS Backup invokes your validation Lambda (if configured) and starts a countdown timer (your restoreTestingPlanValidationWindowHours)
  4. Validation Lambda runs: checks data integrity, calls backup:PutRestoreValidationResult
  5. 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

AlgorithmBehaviorBest for
LATEST_WITHIN_WINDOWSelects the most recent recovery point within selectionWindowDaysStandard testing — always test your most recent backup
RANDOM_WITHIN_WINDOWSelects a random recovery point within the windowLong-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

SymptomCauseFix
Restore testing job status TIMED_OUTValidation Lambda did not call PutRestoreValidationResult within the validation windowIncrease 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 pointsNo recovery points exist in the specified vaults within selectionWindowDaysVerify 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 expectedRecovery point was taken during a low-traffic period; or restore captured an on-demand snapshot before bulk load completedAdjust 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 tableLambda IAM role doesn't have permission on the dynamically-named restored tableUse a resource ARN with * suffix: arn:aws:dynamodb:region:account:table/alivemcp-sessions-restore-test*
Restore testing cleanup deleted the table before validation Lambda ranValidation window is shorter than the Lambda's initialization time plus restore job processing lagIncrease validationWindowHours to at least 2 hours; cold-start Lambda initialization plus DynamoDB table restore can take 20–40 minutes for medium tables