RDS deletion protection is not enabled when DeletionProtection is false on a DB instance or DB cluster. List both with DescribeDBInstances and DescribeDBClusters, then turn it on with ModifyDBInstance or ModifyDBCluster and DeletionProtection: true. The change takes effect at once and causes no downtime. For Aurora, set it on the cluster.
Deletion protection is the cheapest safety net RDS has: one flag, no cost, no restart. It’s on by default when you create a DB instance in the console, but off by default when you create one with the CLI, the API or most infrastructure-as-code tools. So the databases built by your pipelines are exactly the ones most likely to have RDS deletion protection not enabled.
This example gives you a TypeScript script for the AWS SDK for JavaScript v3 that lists every unprotected database in the Regions you choose and, with --apply, turns protection on without touching anything else. It pairs with the audit to find RDS instances without automated backups, which covers what you restore from if a deletion gets through anyway.
What does RDS deletion protection block, and what doesn’t it?
While it’s on, a DeleteDBInstance or DeleteDBCluster call fails, whoever makes it and whatever tool they use. Someone has to modify the database and turn protection off first, which turns a one-step mistake into a deliberate two-step action.
| Action | Blocked by deletion protection? |
|---|---|
| Deleting a standalone DB instance | Yes |
| Deleting an Aurora or Multi-AZ DB cluster | Yes, set on the cluster |
| Deleting DB instances inside a protected Aurora cluster | No, even the last one. The cluster volume and its data stay, and you can add a new instance |
| Deleting manual snapshots | No. Snapshots are separate resources |
DROP TABLE or a bad migration |
No. That’s what point-in-time recovery is for |
Google’s SRE book makes the same distinction in its chapter on data integrity: soft deletion is “the primary defense against developer error”, and backups sit behind it. Deletion protection plays the soft-deletion role for the database resource itself; automated backups and snapshots handle mistakes inside it.
What does the script do?
- Reads clusters
paginateDescribeDBClusterslists Aurora and Multi-AZ DB clusters withDeletionProtectionfalse. - Reads standalone instances
paginateDescribeDBInstances, skipping cluster members (the cluster setting is what counts) and marking read replicas.--skip-replicasleaves them out. - Adds contextShows an
env,environmentorstagetag, and anyPendingModifiedValues, so you can see production databases and queued changes at a glance. - Turns protection on with
--applyModifyDBClusterorModifyDBInstancewithDeletionProtection: trueandApplyImmediately: false.--nameslimits the change to identifiers you list.
Why ApplyImmediately: false? Deletion protection ignores that setting and takes effect at once. Passing true would also push out any change queued for the maintenance window, such as an instance class change, which can cause downtime you didn’t plan.
Prerequisites
- Node.js 18 or later, npm,
tsxand@aws-sdk/client-rds. - A read-only profile for the report and a separate one with the modify actions for
--apply; the guide to AWS SDK v3 credential providers with fromIni and fromSSO shows how the SDK picks them up. - Agreement on which databases should stay deletable, such as short-lived test instances your pipeline tears down.
Which IAM permissions does it need?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadDatabases",
"Effect": "Allow",
"Action": [
"rds:DescribeDBInstances",
"rds:DescribeDBClusters"
],
"Resource": "*"
},
{
"Sid": "EnableProtectionWithApply",
"Effect": "Allow",
"Action": [
"rds:ModifyDBInstance",
"rds:ModifyDBCluster"
],
"Resource": [
"arn:aws:rds:*:123456789012:db:*",
"arn:aws:rds:*:123456789012:cluster:*"
]
}
]
}
Drop the second statement for an audit-only role. The same modify actions can also turn protection off, so keep this role for the people who own the databases. The free IAM policy generator for TypeScript code derives the list from the script, and the guide to review a generated IAM policy for least privilege helps you tighten it.
The script to fix RDS deletion protection not enabled
// find-rds-without-deletion-protection.ts
// Lists RDS DB instances and Aurora / Multi-AZ DB clusters where deletion protection is off.
// Report only unless you pass --apply, which turns deletion protection on for the flagged databases.
// Usage:
// npx tsx find-rds-without-deletion-protection.ts [--regions us-east-1,eu-west-1] [--names db1,db2] [--skip-replicas] [--apply]
import { ModifyDBClusterCommand, ModifyDBInstanceCommand, RDSClient, paginateDescribeDBClusters, paginateDescribeDBInstances, type Tag } from "@aws-sdk/client-rds";
const args = process.argv.slice(2);
const flag = (name: string): string | undefined => {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
};
const list = (v: string | undefined) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
const regions = list(flag("--regions") ?? process.env.AWS_REGION ?? "us-east-1");
const onlyNames = new Set(list(flag("--names"))); // limit --apply to these identifiers
const skipReplicas = args.includes("--skip-replicas");
const apply = args.includes("--apply");
interface Row {
Region: string;
Kind: "cluster" | "instance" | "replica";
Name: string;
Engine: string;
Env: string;
Pending: string;
Result: string;
}
const envTag = (tags: Tag[] | undefined) =>
tags?.find((t) => ["env", "environment", "stage"].includes((t.Key ?? "").toLowerCase()))?.Value ?? "";
async function scanRegion(region: string): Promise<Row[]> {
const rds = new RDSClient({ region });
const rows: Row[] = [];
// Aurora and Multi-AZ DB clusters: protection is a cluster setting.
for await (const page of paginateDescribeDBClusters({ client: rds }, {})) {
for (const c of page.DBClusters ?? []) {
if (c.DeletionProtection) continue;
rows.push({
Region: region,
Kind: "cluster",
Name: c.DBClusterIdentifier ?? "?",
Engine: c.Engine ?? "?",
Env: envTag(c.TagList),
Pending: c.PendingModifiedValues ? Object.keys(c.PendingModifiedValues).join(" ") : "",
Result: "OFF",
});
}
}
// Standalone DB instances and read replicas. Cluster members inherit protection from the cluster row.
for await (const page of paginateDescribeDBInstances({ client: rds }, {})) {
for (const db of page.DBInstances ?? []) {
if (db.DBClusterIdentifier || db.DeletionProtection) continue;
const replica = Boolean(db.ReadReplicaSourceDBInstanceIdentifier || db.ReadReplicaSourceDBClusterIdentifier);
if (replica && skipReplicas) continue;
rows.push({
Region: region,
Kind: replica ? "replica" : "instance",
Name: db.DBInstanceIdentifier ?? "?",
Engine: db.Engine ?? "?",
Env: envTag(db.TagList),
Pending: db.PendingModifiedValues ? Object.keys(db.PendingModifiedValues).join(" ") : "",
Result: "OFF",
});
}
}
if (!apply) return rows;
for (const row of rows) {
if (onlyNames.size && !onlyNames.has(row.Name)) {
row.Result = "OFF (not in --names)";
continue;
}
try {
// ApplyImmediately stays false: deletion protection takes effect at once anyway, and true would
// also push any pending modifications (instance class, engine version) out right now.
if (row.Kind === "cluster") {
await rds.send(new ModifyDBClusterCommand({ DBClusterIdentifier: row.Name, DeletionProtection: true, ApplyImmediately: false }));
} else {
await rds.send(new ModifyDBInstanceCommand({ DBInstanceIdentifier: row.Name, DeletionProtection: true, ApplyImmediately: false }));
}
row.Result = "ENABLED";
} catch (err) {
row.Result = `error: ${err instanceof Error ? err.name : String(err)}`;
}
}
return rows;
}
async function main(): Promise<void> {
const rows: Row[] = [];
for (const region of regions) {
try {
rows.push(...(await scanRegion(region)));
} catch (err) {
console.error(`${region}: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
}
}
if (rows.length) console.table(rows);
const enabled = rows.filter((r) => r.Result === "ENABLED").length;
console.log(`${rows.length} databases without deletion protection in ${regions.join(", ")}`);
console.log(apply ? `Deletion protection turned on for ${enabled} of them.` : "Report only. Re-run with --apply to turn deletion protection on.");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
How do you run it?
npm install @aws-sdk/client-rds
npm install --save-dev tsx typescript @types/node
# Report for two Regions
AWS_PROFILE=readonly npx tsx find-rds-without-deletion-protection.ts --regions us-east-1,eu-west-1
# Protect two named databases
AWS_PROFILE=rds-admin npx tsx find-rds-without-deletion-protection.ts --regions us-east-1 --names orders-aurora,payments-pg --apply
Sample output
┌─────────┬─────────────┬────────────┬─────────────────┬─────────────────────┬────────┬───────────────────┬────────────────────────┐
│ (index) │ Region │ Kind │ Name │ Engine │ Env │ Pending │ Result │
├─────────┼─────────────┼────────────┼─────────────────┼─────────────────────┼────────┼───────────────────┼────────────────────────┤
│ 0 │ 'us-east-1' │ 'cluster' │ 'orders-aurora' │ 'aurora-postgresql' │ 'prod' │ '' │ 'ENABLED' │
│ 1 │ 'us-east-1' │ 'instance' │ 'payments-pg' │ 'postgres' │ 'prod' │ 'DBInstanceClass' │ 'ENABLED' │
│ 2 │ 'us-east-1' │ 'instance' │ 'ci-tmp-4821' │ 'mysql' │ 'ci' │ '' │ 'OFF (not in --names)' │
└─────────┴─────────────┴────────────┴─────────────────┴─────────────────────┴────────┴───────────────────┴────────────────────────┘
3 databases without deletion protection in us-east-1
Deletion protection turned on for 2 of them.
Names are illustrative. payments-pg has an instance class change queued; because the script sends ApplyImmediately: false, that change still waits for the maintenance window. The CI database stays deletable on purpose. Protected databases still need major version upgrades; the script to find RDS and Aurora databases on extended support charges shows which ones already pay per-vCPU charges for staying on an old version.
What else protects a database from being deleted?
- Set it in code. Add
deletion_protection = truein Terraform,DeletionProtection: truein CloudFormation, ordeletionProtection: truein CDK, or the next deploy can switch it back off. Pair it with aDeletionPolicyofSnapshotorRetainin CloudFormation. - Take a final snapshot. When you do delete, keep a final DB snapshot and consider retaining automated backups. Manual snapshots outlive the instance, which is why the script to find and delete old RDS manual snapshots exists.
- Watch the switch. Turning protection off is a
ModifyDBInstanceorModifyDBClustercall recorded in CloudTrail; the check to confirm CloudTrail is logging in every AWS Region makes sure that record exists. - Act fast after a mistake. AWS documents that recovery of an unintentionally deleted RDS instance might be possible for up to six days if you contact AWS Support immediately. Don’t count on it.
The same thinking applies to other stores: enable DynamoDB point-in-time recovery on every table and find S3 buckets without versioning enabled.
Troubleshooting
InvalidDBInstanceStateFaultorInvalidDBClusterStateFaulton--apply. The database is in a state that doesn’t accept changes, such as creating, modifying or stopped. Retry once it’savailable.- An Aurora instance still deletes. That’s expected. Protection covers the cluster, not its member instances.
- Protection turns itself off again. Your infrastructure-as-code still says false and reverts it on the next deploy. Fix the template.
AccessDeniedon modify. Check the resource ARNs and any service control policy; the guide to troubleshoot AWS IAM access denied errors walks through it.
Ask ChatWithCloud instead
To just see the list, ask ChatWithCloud “Which RDS databases in us-east-1 don’t have deletion protection?” It writes AWS SDK for JavaScript v2 code, runs it locally with your AWS profile and explains the answer; how ChatWithCloud answers AWS questions shows the loop. It runs generated code without a confirmation step, so connect ChatWithCloud with a read-only AWS profile and make the change with the script above.
Frequently asked questions
How do I enable deletion protection on an existing RDS instance?
Run aws rds modify-db-instance --db-instance-identifier mydb --deletion-protection. For Aurora, use aws rds modify-db-cluster --db-cluster-identifier mycluster --deletion-protection.
Does enabling RDS deletion protection cause downtime?
No. The change takes effect immediately, ignores the apply-immediately setting and doesn’t restart the database.
Is RDS deletion protection enabled by default?
For DB instances created in the console, yes. For databases created with the AWS CLI or API it’s off unless you pass it. Aurora clusters created in the console as production get it by default.
Does deletion protection cost anything?
No. It’s a setting on the database with no charge.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud