Photo by Marc PEZIN on Unsplash
To enable DynamoDB point-in-time recovery on every table, list the tables with ListTables, check each one with DescribeContinuousBackups, and call UpdateContinuousBackups with PointInTimeRecoveryEnabled: true where it’s off. You can set RecoveryPeriodInDays from 1 to 35 (the default is 35). PITR is billed per GB-month of table size, whatever period you choose.
This example is for engineers who own a handful to a few hundred DynamoDB tables and want every one of them recoverable after a bad deploy or a buggy migration script. The script uses AWS SDK for JavaScript v3, reports PITR status, size and an estimated monthly cost per table, and turns point-in-time recovery on only when you pass --apply.
It sits next to the example that finds overprovisioned DynamoDB read and write capacity: that one trims cost, this one buys insurance. Both are part of our library of AWS SDK v3 practical examples.
What does point-in-time recovery give you?
PITR keeps continuous backups of a table with per-second granularity. You can restore to any second between EarliestRestorableDateTime and LatestRestorableDateTime, which is typically five minutes before now. A few facts from the DynamoDB developer guide shape how you use it:
- The recovery period is 1 to 35 days.
RecoveryPeriodInDaysdefaults to 35. Shortening it doesn’t lower the price, because the charge is based on the size of the table and its local secondary indexes. - A restore always creates a new table. You never overwrite the original in place. Auto scaling policies, IAM policies, CloudWatch alarms, tags, Streams, TTL and the PITR setting itself aren’t carried over; you set them on the new table.
- Turning PITR off and on again resets the window. The earliest restore point starts over from the moment you re-enable it.
- Deleting a PITR-enabled table leaves a system backup named
table-name$DeletedTableBackup, kept for 35 days at no extra cost. Without PITR, a deleted table is gone unless you have another backup of it. - Global tables are per replica. You enable PITR on each local replica, and a restore produces an independent table outside the global table.
One trap in the API: ContinuousBackupsStatus is always ENABLED. The field that tells you whether you can restore is PointInTimeRecoveryDescription.PointInTimeRecoveryStatus, which is DISABLED on new tables until you turn it on. The script reads the second one.
How much does PITR cost?
As of September 2026, continuous backup storage in us-east-1 costs $0.20 per GB-month, and restoring from a backup costs $0.15 per GB of data restored, according to the AWS Price List for DynamoDB (published 11 September 2026) and the Amazon DynamoDB pricing page. Other Regions differ, so pass your Region’s rate with --price.
| Tables without PITR | Size (table + LSIs) | Arithmetic | Added per month |
|---|---|---|---|
| orders | 42 GB | 42 × $0.20 | $8.40 |
| sessions | 3.5 GB | 3.5 × $0.20 | $0.70 |
| audit-events | 210 GB | 210 × $0.20 | $42.00 |
| Total | 255.5 GB | 255.5 × $0.20 | $51.10 |
A full restore of orders would add 42 × $0.15 = $6.30 once. Large, append-only tables such as audit logs dominate the bill; if the data is reproducible from another source, that table may not need PITR at all. Seen from the cost side, the same thinking applies to S3 storage class choices for backup data.
Prerequisites
- Node.js 20 or later, npm and
tsx. - The
@aws-sdk/client-dynamodbpackage. - A profile with a Region set, or
AWS_REGION. DynamoDB tables are Regional, so run the script once per Region you use. If you’re unsure which credentials the SDK picks up, see how AWS SDK v3 credential providers such as fromIni and fromSSO resolve them.
Which IAM permissions does it need?
The report needs three read actions. dynamodb:UpdateContinuousBackups is only for --apply; leave that statement out of an audit role. ListTables doesn’t support resource-level permissions, so it uses *; the rest can be scoped to table ARNs.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListTables",
"Effect": "Allow",
"Action": "dynamodb:ListTables",
"Resource": "*"
},
{
"Sid": "ReadPitrStatus",
"Effect": "Allow",
"Action": ["dynamodb:DescribeTable", "dynamodb:DescribeContinuousBackups"],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/*"
},
{
"Sid": "OptionalEnablePitr",
"Effect": "Allow",
"Action": "dynamodb:UpdateContinuousBackups",
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/*"
}
]
}
Replace the account ID and Region. If you change the script, the IAM policy generator for TypeScript code drafts a policy from your version, and the checklist to review a generated IAM policy for least privilege helps you tighten it.
The script to enable DynamoDB point-in-time recovery
// enable-dynamodb-pitr.ts
// Reports point-in-time recovery (PITR) status for every DynamoDB table in one Region,
// with table size and an estimated monthly PITR cost. Read-only by default.
// With --apply it turns PITR on for tables where it's off, using --days (1-35, default 35).
// Usage: npx tsx enable-dynamodb-pitr.ts [--days 35] [--price 0.20] [--apply]
import {
DynamoDBClient,
DescribeContinuousBackupsCommand,
DescribeTableCommand,
UpdateContinuousBackupsCommand,
paginateListTables,
} from "@aws-sdk/client-dynamodb";
const apply = process.argv.includes("--apply");
function numberArg(name: string, fallback: number): number {
const i = process.argv.indexOf(name);
if (i === -1) return fallback;
const value = Number(process.argv[i + 1]);
if (!Number.isFinite(value)) throw new Error(`${name} needs a number`);
return value;
}
const days = numberArg("--days", 35);
if (!Number.isInteger(days) || days < 1 || days > 35) throw new Error("--days must be 1 to 35");
// USD per GB-month of table + LSI size. Default: us-east-1 list price, September 2026.
const pricePerGbMonth = numberArg("--price", 0.2);
type TableRow = {
table: string;
status: string;
pitr: string;
days: number | string;
sizeGB: number;
estMonthlyUSD: number;
};
const ddb = new DynamoDBClient({ maxAttempts: 5 });
async function describe(table: string): Promise<TableRow> {
const { Table } = await ddb.send(new DescribeTableCommand({ TableName: table }));
// PITR is billed on table data plus local secondary indexes.
const lsiBytes = (Table?.LocalSecondaryIndexes ?? []).reduce((sum, i) => sum + (i.IndexSizeBytes ?? 0), 0);
const sizeGB = ((Table?.TableSizeBytes ?? 0) + lsiBytes) / 1024 ** 3;
const { ContinuousBackupsDescription: cb } = await ddb.send(
new DescribeContinuousBackupsCommand({ TableName: table }),
);
const pitr = cb?.PointInTimeRecoveryDescription;
return {
table,
status: Table?.TableStatus ?? "?",
pitr: pitr?.PointInTimeRecoveryStatus ?? "UNKNOWN",
days: pitr?.RecoveryPeriodInDays ?? "-",
sizeGB: Number(sizeGB.toFixed(3)),
estMonthlyUSD: Number((sizeGB * pricePerGbMonth).toFixed(2)),
};
}
async function main(): Promise<void> {
const region = await ddb.config.region();
const rows: TableRow[] = [];
for await (const page of paginateListTables({ client: ddb }, {})) {
for (const name of page.TableNames ?? []) rows.push(await describe(name));
}
console.log(`Region ${region}: ${rows.length} tables`);
console.table(rows);
const targets = rows.filter((r) => r.pitr === "DISABLED");
const addedCost = targets.reduce((sum, r) => sum + r.estMonthlyUSD, 0);
console.log(
`${targets.length} tables without PITR. Enabling it on all of them adds about ` +
`$${addedCost.toFixed(2)}/month at $${pricePerGbMonth}/GB-month.`,
);
if (targets.length === 0) return;
console.log(`\n${apply ? "Enabling" : "Dry run: would enable"} PITR (${days} days) on:`);
for (const r of targets) {
if (r.status !== "ACTIVE") {
console.log(` ${r.table}: skipped, table status is ${r.status}`);
continue;
}
console.log(` ${r.table}`);
if (!apply) continue;
const out = await ddb.send(
new UpdateContinuousBackupsCommand({
TableName: r.table,
PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true, RecoveryPeriodInDays: days },
}),
);
const status = out.ContinuousBackupsDescription?.PointInTimeRecoveryDescription?.PointInTimeRecoveryStatus;
console.log(` PointInTimeRecoveryStatus=${String(status)}`);
}
if (!apply) console.log("Re-run with --apply to make the change.");
}
main().catch((err: unknown) => {
console.error(err);
process.exit(1);
});
The safety rails: nothing changes without --apply; tables that aren’t ACTIVE are skipped; tables that already have PITR are left alone, including their recovery period; and the only write call is UpdateContinuousBackups. The size comes from DescribeTable, which DynamoDB updates roughly every six hours, so treat the cost column as an estimate.
How do you run it?
npm install @aws-sdk/client-dynamodb
npm install --save-dev tsx typescript
# Report only
AWS_PROFILE=audit AWS_REGION=us-east-1 npx tsx enable-dynamodb-pitr.ts
# Dry run with a 14-day window and another Region's price
AWS_PROFILE=audit AWS_REGION=eu-west-1 npx tsx enable-dynamodb-pitr.ts --days 14 --price 0.22
# Make the change
AWS_PROFILE=ops-admin AWS_REGION=us-east-1 npx tsx enable-dynamodb-pitr.ts --apply
The 0.22 above is a placeholder; look up your Region’s rate before you rely on the estimate.
Sample output
Region us-east-1: 4 tables
┌─────────┬────────────────┬──────────┬────────────┬──────┬────────┬───────────────┐
│ (index) │ table │ status │ pitr │ days │ sizeGB │ estMonthlyUSD │
├─────────┼────────────────┼──────────┼────────────┼──────┼────────┼───────────────┤
│ 0 │ 'audit-events' │ 'ACTIVE' │ 'DISABLED' │ '-' │ 210 │ 42 │
│ 1 │ 'customers' │ 'ACTIVE' │ 'ENABLED' │ 35 │ 1.2 │ 0.24 │
│ 2 │ 'orders' │ 'ACTIVE' │ 'DISABLED' │ '-' │ 42 │ 8.4 │
│ 3 │ 'sessions' │ 'ACTIVE' │ 'DISABLED' │ '-' │ 3.5 │ 0.7 │
└─────────┴────────────────┴──────────┴────────────┴──────┴────────┴───────────────┘
3 tables without PITR. Enabling it on all of them adds about $51.10/month at $0.2/GB-month.
Dry run: would enable PITR (35 days) on:
audit-events
orders
sessions
Re-run with --apply to make the change.
Decide per table before you apply. orders is the obvious yes. sessions is cheap but short-lived data you may never restore. For audit-events, compare $42 a month with what it would cost to rebuild the table from its source. A table with no reads or writes for a month may not need PITR at all; the script to find unused DynamoDB tables helps you decide whether to protect it or delete it.
How do you restore a table from PITR?
- Pick the momentFind the last good time from your deploy log or CloudWatch metrics, and confirm it falls between
EarliestRestorableDateTimeandLatestRestorableDateTime. - Restore to a new tableCall
RestoreTableToPointInTime(oraws dynamodb restore-table-to-point-in-time) with a new target table name andRestoreDateTime. Cross-Region restores are possible too. - Reapply what isn’t restoredAuto scaling, IAM policies, alarms, tags, Streams, TTL and PITR itself must be set again on the new table.
- Switch or copy backEither point the application at the new table, or copy the damaged items back from it into the original. A key-condition query per affected partition, as in the guide to query DynamoDB with AWS SDK v3, finds those items without a full Scan.
Rehearse this once on a small table. A restore you’ve never tried isn’t a recovery plan. PITR is the backstop; the first defence is writes that can’t clobber data, such as the conditional updates and optimistic locking in the guide to update DynamoDB items with AWS SDK v3 using conditions and counters.
Troubleshooting
AccessDeniedExceptiononDescribeContinuousBackups. The audit role lacks the action, or a service control policy denies it. The steps to troubleshoot an AWS IAM access denied error walk through each policy layer.ContinuousBackupsUnavailableException. The API’s description is “Backups have not yet been enabled for this table”. Wait and rerun for that table.TableNotFoundException. The table was deleted between listing and updating, or you’re in the wrong Region.- The earliest restore point is recent. PITR was turned off and on again, or the recovery period was shortened, which drops older restore points immediately.
Ask ChatWithCloud instead
From a read-only profile you can ask ChatWithCloud “Which DynamoDB tables don’t have point-in-time recovery enabled?”. It writes AWS SDK for JavaScript v2 code, runs it on your machine with your profile and summarizes the result; how ChatWithCloud runs AWS SDK code on your machine shows the loop. It uses one profile and Region per session and runs changes without a confirmation step, so keep dynamodb:UpdateContinuousBackups out of the profile unless you intend to change tables. The guide to analyze your AWS security posture with an AI CLI has more questions like this.
For the rest of a data-protection review, pair this with the script to find public EBS and RDS snapshots and the one to find unencrypted EBS volumes and turn on default encryption.
Frequently asked questions
How do I enable DynamoDB point-in-time recovery with the AWS CLI?
Run aws dynamodb update-continuous-backups --table-name orders --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true,RecoveryPeriodInDays=35, once per table.
Does a shorter recovery period make PITR cheaper?
No. The charge depends on table and local secondary index size, not on RecoveryPeriodInDays.
Can I restore a DynamoDB table in place?
No. Point-in-time recovery always restores into a new table, which you then switch to or copy data back from.
Is PITR on by default for new DynamoDB tables?
No. New tables report PointInTimeRecoveryStatus as DISABLED until you enable it, even though ContinuousBackupsStatus says ENABLED.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud