This check flags RDS instances with zero database connections over the last 7 days. Idle databases waste money and widen your attack surface, so confirm they are truly unused, snapshot them, then stop or delete them.
An RDS instance with no connections for a week is a strong signal that something was left behind. Maybe it was a staging database for a project that shipped months ago, a copy spun up for a one-off migration, or a service that got decommissioned without anyone cleaning up its backing store. Whatever the reason, it is still running, still billing, and still part of your security perimeter.
The rds_unused check looks at connection metrics and surfaces instances that have recorded no client activity in the past 7 days. It gives you a shortlist of candidates worth investigating before you keep paying for compute and storage you do not use.
What this check detects
Lensix evaluates the CloudWatch DatabaseConnections metric for each RDS instance over a rolling 7 day window. When the sum of connections across that period is zero, the instance is flagged as appearing unused.
This applies to standard RDS engines such as PostgreSQL, MySQL, MariaDB, Oracle, and SQL Server. The check is intentionally conservative: it only reports instances that show no connection activity at all, which keeps false positives low.
Note: A flagged instance is not automatically safe to delete. The DatabaseConnections metric only counts active client sessions. An instance could still be referenced by application config, DNS records, or a disaster recovery plan even if nothing has connected recently. Treat the flag as a prompt to investigate, not a verdict.
Why it matters
An idle RDS instance is rarely harmless. It costs you on three fronts.
Cost
RDS bills you for instance hours, allocated storage, provisioned IOPS, backup storage beyond the free tier, and any read replicas. A single db.r6g.xlarge running multi-AZ can run several hundred dollars a month before storage. Multiply that across a handful of forgotten databases and the waste adds up fast. Unlike EC2, you cannot reduce RDS cost to zero by stopping the instance for long, because AWS automatically restarts a stopped instance after 7 days.
Security exposure
Every running database is an asset an attacker can target. An unused instance is worse than an active one because nobody is watching it. Patches may lag, the security group may be overly permissive, and unusual access would go unnoticed because there is no baseline of normal traffic.
Warning: Forgotten databases are a common source of breaches. They frequently hold real production data copied during a migration or test, sit on default or weak credentials, and never get rotated. If that instance is also publicly accessible, you have a credential-stuffing or brute-force target with sensitive data behind it.
Compliance drift
Auditors expect you to know what data you store and where. An undocumented database that still holds customer records is a data inventory gap. Under frameworks like SOC 2, PCI DSS, and GDPR, you are responsible for that data whether or not anyone is using the instance.
How to fix it
Work through this in order. The goal is to confirm the instance is genuinely unused, preserve a recovery path, then remove the cost.
Step 1: Confirm it is actually idle
Before touching anything, double-check the connection metrics yourself and extend the window further back. A weekly batch job or quarterly report could connect on a schedule longer than 7 days.
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS \
--metric-name DatabaseConnections \
--dimensions Name=DBInstanceIdentifier,Value=my-instance \
--start-time "$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 86400 \
--statistics Maximum \
--query 'Datapoints[].{Time:Timestamp,Max:Maximum}' \
--output table
If every Maximum is 0 across 30 days, you have a much stronger case. Also check who owns the instance using its tags and trace any application or DNS entry that points at the endpoint.
aws rds describe-db-instances \
--db-instance-identifier my-instance \
--query 'DBInstances[0].{Endpoint:Endpoint.Address,Tags:TagList,PubliclyAccessible:PubliclyAccessible}'
Step 2: Take a final snapshot
Always snapshot before you stop or delete. A manual snapshot persists independently of the instance and costs only for the storage it consumes.
aws rds create-db-snapshot \
--db-instance-identifier my-instance \
--db-snapshot-identifier my-instance-final-2024-06
Step 3: Stop the instance (reversible)
If you are not yet certain, stop the instance rather than deleting it. This halts compute charges while you wait for someone to notice. You still pay for storage and snapshots.
aws rds stop-db-instance --db-instance-identifier my-instance
Warning: AWS automatically starts a stopped RDS instance after 7 days. If you want a longer pause without deleting, schedule a recurring stop with an EventBridge rule, or commit to deleting the instance once you have the snapshot.
Step 4: Delete the instance
Once you are confident and have a snapshot, remove the instance for good.
Danger: Deletion is irreversible. Confirm the final snapshot exists and is in available status before running this. If the instance is part of a cluster or has read replicas, remove the replicas first. Once deleted, restoring requires recreating the instance from the snapshot, which changes the endpoint hostname.
# Verify the snapshot is ready first
aws rds describe-db-snapshots \
--db-snapshot-identifier my-instance-final-2024-06 \
--query 'DBSnapshots[0].Status'
# Delete with an extra automatic final snapshot for safety
aws rds delete-db-instance \
--db-instance-identifier my-instance \
--final-db-snapshot-identifier my-instance-delete-final \
--no-skip-final-snapshot
Tip: Snapshots are far cheaper than running instances. Keeping a final snapshot for 90 days then deleting it gives you a recovery window without the ongoing cost of a live database. Tag the snapshot with the deletion date and owner so it does not become its own orphan.
How to prevent it from happening again
Idle databases are an inevitability at scale unless you build guardrails. Here is how to stop them from accumulating.
Tag everything at creation
Require an owner, environment, and expiry tag on every database. Enforce it with an AWS Organizations tag policy or a Service Control Policy so untagged resources cannot be created.
{
"tags": {
"owner": {
"tag_key": { "@@assign": "owner" },
"enforced_for": { "@@assign": ["rds:db"] }
},
"expiry": {
"tag_key": { "@@assign": "expiry" },
"enforced_for": { "@@assign": ["rds:db"] }
}
}
}
Gate ephemeral databases in IaC
Test and migration databases should be defined in their own short-lived Terraform stacks so they are destroyed when the work is done. Add a lifecycle tag so reviewers can spot temporary resources during pull requests.
resource "aws_db_instance" "migration_temp" {
identifier = "migration-temp"
engine = "postgres"
instance_class = "db.t4g.medium"
allocated_storage = 20
skip_final_snapshot = false
publicly_accessible = false
tags = {
owner = "data-platform"
environment = "ephemeral"
expiry = "2024-07-01"
}
}
Run a scheduled idle audit
Automate the same query Lensix runs and report on it weekly. This Lambda-friendly snippet lists every instance with zero connections in the last 7 days so you can route the list to the owning team.
for db in $(aws rds describe-db-instances \
--query 'DBInstances[].DBInstanceIdentifier' --output text); do
total=$(aws cloudwatch get-metric-statistics \
--namespace AWS/RDS --metric-name DatabaseConnections \
--dimensions Name=DBInstanceIdentifier,Value="$db" \
--start-time "$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 604800 --statistics Sum \
--query 'Datapoints[0].Sum' --output text)
if [ "$total" = "0.0" ] || [ "$total" = "None" ]; then
echo "IDLE: $db"
fi
done
Tip: Continuous monitoring beats a manual script. Lensix runs rds_unused across all your accounts automatically and tracks each finding over time, so you can see whether an instance has been idle for one week or three months without writing your own tooling.
Best practices
- Set a connection alarm on new databases. A CloudWatch alarm that fires when
DatabaseConnectionsstays at zero for several days catches abandonment early. - Default to private. Never set
PubliclyAccessibleto true unless there is a documented reason. An idle private database is a smaller risk than an idle public one. - Right-size before you delete. Some flagged instances are low-traffic rather than abandoned. If a database serves a real but tiny workload, move it to a smaller instance class or consolidate it instead of removing it.
- Keep a data inventory. Map every database to the data classification it holds. This makes the delete-or-keep decision obvious and satisfies auditors.
- Lifecycle your snapshots. Orphaned snapshots accumulate cost quietly. Tag them with an expiry and clean them up on a schedule.
- Make cleanup someone's job. Assign idle-resource review to a rotating on-call or a monthly cost review so findings get acted on rather than ignored.
An unused RDS instance is a small problem that quietly grows. Catch it early, confirm it carefully, snapshot it, and remove it. You cut spend and shrink your attack surface in one move.

