· 12 min read

How to Reduce AWS RDS Costs: Right-Sizing, Reserved Instances, and Storage Optimization

RDS is one of the top five cost drivers in most AWS accounts. It runs 24/7, provisions resources whether you use them or not, and charges you across multiple dimensions: compute, storage, I/O, backups, and data transfer.

The problem isn’t that RDS is expensive. It’s that most teams overprovision it during setup and never revisit those decisions. A database that was right-sized for launch-day traffic might be 3x larger than what you actually need six months later. Multi-AZ might be enabled on dev environments. Snapshots pile up. Previous-generation instances keep running because nobody wants to touch a production database.

This guide covers the highest-impact RDS cost optimizations with CLI commands you can run today. These are the same patterns that CostPatrol’s RDS detection rules check automatically.

Understanding RDS Pricing Components

Before optimizing, you need to know where your money goes. RDS charges across five dimensions:

ComponentWhat You Pay ForTypical Cost (us-east-1)
ComputeInstance hours, billed per second$0.30/hr for db.m6g.large
StorageProvisioned GB per month$0.08/GB for gp3
IOPSI/O operations (gp3 above 3,000 baseline)$0.005 per provisioned IOP
BackupsStorage beyond free tier (1x DB size)$0.095/GB-month
Data TransferCross-AZ, cross-region, and internet egress$0.01-$0.02/GB cross-AZ

Compute is almost always the largest line item, typically 60-70% of your total RDS spend. That’s where the biggest wins are.

List your running RDS instances with their classes and engines:

aws rds describe-db-instances \
  --query 'DBInstances[].{ID:DBInstanceIdentifier,Class:DBInstanceClass,Engine:Engine,MultiAZ:MultiAZ,Storage:AllocatedStorage,StorageType:StorageType}' \
  --output table

1. Identify and Remove Idle Instances (RDS-O001)

The simplest win: find databases nobody is using. This is more common than you’d expect. A developer spins up an RDS instance for a feature branch, the feature ships, the database stays. A staging environment gets abandoned. A migration completes but the old database keeps running.

An RDS instance is idle when the average number of database connections has been below 1 for the last 7 days AND the combined ReadIOPS and WriteIOPS are below 20 per day on average.

Check connection count for a specific instance over the last 7 days:

aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name DatabaseConnections \
  --dimensions Name=DBInstanceIdentifier,Value=my-database \
  --start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 86400 \
  --statistics Average \
  --output table

Check I/O activity for the same period:

aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name ReadIOPS \
  --dimensions Name=DBInstanceIdentifier,Value=my-database \
  --start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 86400 \
  --statistics Average \
  --output table

If both connections and IOPS are near zero, take a final snapshot and delete the instance:

aws rds delete-db-instance \
  --db-instance-identifier my-idle-database \
  --final-db-snapshot-identifier my-idle-database-final-snap \
  --no-delete-automated-backups

A single idle db.m6g.large costs $219/month doing nothing. Across a team with multiple environments, idle instances easily add up to thousands per year.

CostPatrol’s RDS-O001 rule scans all instances in your account and flags any with near-zero connections and I/O over a 7-day window.

2. Right-Size Overprovisioned Instances

After eliminating idle instances, the next target is instances that are running but barely working. The AWS right-sizing whitepaper sets a clear threshold: if maximum CPU utilization stays below 40% over a four-week period, the instance is a candidate for downsizing.

Get max CPU utilization for the last 28 days:

aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name CPUUtilization \
  --dimensions Name=DBInstanceIdentifier,Value=my-database \
  --start-time $(date -u -d '28 days ago' +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 2419200 \
  --statistics Maximum \
  --output text

Check freeable memory:

aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name FreeableMemory \
  --dimensions Name=DBInstanceIdentifier,Value=my-database \
  --start-time $(date -u -d '28 days ago' +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 2419200 \
  --statistics Minimum \
  --output text

One important caveat on memory: database engines intentionally consume most available memory. MySQL’s InnoDB buffer pool defaults to 75% of instance memory, so an “idle” MySQL instance still shows 75% memory used. Don’t right-size based on memory alone. If swap usage is near zero and your buffer cache hit ratio stays above 99.9%, memory is adequate.

Right-sizing decision matrix:

Max CPU (28d)Min FreeableMemoryAction
Under 20%Over 50% of instance RAMDrop two instance sizes
20-40%Over 30% of instance RAMDrop one instance size
40-70%10-30% of instance RAMWell-sized
Over 70%Under 10% of instance RAMConsider upsizing

Example savings from right-sizing (us-east-1, PostgreSQL, On-Demand):

FromToMonthly CostMonthly Savings
db.r6g.2xlarge ($0.68/hr)db.r6g.xlarge ($0.34/hr)$496 to $248$248 (50%)
db.m6g.xlarge ($0.61/hr)db.m6g.large ($0.30/hr)$445 to $219$226 (51%)
db.r6g.xlarge ($0.34/hr)db.r6g.large ($0.34/hr)$379 to $248$131 (35%)

Modify the instance class (applies during next maintenance window):

aws rds modify-db-instance \
  --db-instance-identifier my-database \
  --db-instance-class db.m6g.large \
  --apply-immediately

Note: --apply-immediately causes a brief outage during the instance class change. For production databases, omit this flag to apply during the maintenance window instead.

3. Migrate Off Previous-Generation Instances (RDS-O002)

If you’re still running db.m4, db.m5, db.r4, or db.r5 instances, you’re leaving money on the table. AWS Graviton-based instances (db.m6g, db.r6g, db.m7g, db.r7g) deliver up to 40% better price-performance compared to their Intel/AMD equivalents.

AWS has started the end-of-support process for db.m4 instances across multiple engines. Don’t wait for forced migration.

Find all previous-generation RDS instances in your account:

aws rds describe-db-instances \
  --query 'DBInstances[?contains(DBInstanceClass, `db.m4`) || contains(DBInstanceClass, `db.m5`) || contains(DBInstanceClass, `db.r4`) || contains(DBInstanceClass, `db.r5`) || contains(DBInstanceClass, `db.t2`)].{ID:DBInstanceIdentifier,Class:DBInstanceClass,Engine:Engine}' \
  --output table

Graviton migration pricing comparison (us-east-1, PostgreSQL):

Previous GenCurrent Gen (Graviton)Hourly Rate ChangePerformance Gain
db.m5.large ($0.34/hr)db.m6g.large ($0.30/hr)-12%+30-40%
db.r5.large ($0.38/hr)db.r6g.large ($0.34/hr)-11%+30-40%
db.m5.xlarge ($0.69/hr)db.m6g.xlarge ($0.61/hr)-12%+30-40%

The savings look modest in absolute terms (10-12% on the hourly rate), but the real win is the performance improvement. A db.m6g.large can often handle the workload of a db.m5.xlarge, which means you can downsize AND upgrade simultaneously.

CostPatrol’s RDS-O002 rule flags any instance running on a previous-generation class and calculates the potential savings from upgrading to the current Graviton equivalent.

4. Reserved Instances for Steady-State Databases

If a database runs 24/7 and you’re confident it’ll exist for the next year, Reserved Instances are the most straightforward way to cut costs. The discounts are significant:

TermPayment OptionTypical Savings vs On-Demand
1-yearNo Upfront~30%
1-yearAll Upfront~38%
3-yearNo Upfront~45%
3-yearPartial Upfront~60%
3-yearAll Upfront~63%

Example: db.r6g.large in us-east-1 running PostgreSQL

  • On-Demand: $248/month ($2,976/year)
  • 1-year All Upfront RI: ~$154/month effective ($1,845/year). Saves $1,131/year.
  • 3-year All Upfront RI: ~$92/month effective ($1,100/year). Saves $1,876/year.

Check your current Reserved Instance coverage:

aws rds describe-reserved-db-instances \
  --query 'ReservedDBInstances[].{ID:ReservedDBInstancesId,Class:DBInstanceClass,Engine:ProductDescription,State:State,Remaining:Duration,Count:DBInstanceCount}' \
  --output table

View available RI offerings for your instance type:

aws rds describe-reserved-db-instances-offerings \
  --db-instance-class db.r6g.large \
  --product-description postgresql \
  --query 'ReservedDBInstancesOfferings[].{Offering:ReservedDBInstancesOfferingId,Term:Duration,Payment:OfferingType,Fixed:FixedPrice,Recurring:RecurringCharges[0].RecurringChargeAmount}' \
  --output table

Right-size first, reserve second. Buying an RI for an overprovisioned instance locks you into that waste for 1-3 years. Get the instance class right, run it for a few weeks to confirm stability, then commit.

5. Storage Optimization (RDS-O003)

RDS storage costs are based on what you provision, not what you use. If you allocated 500 GB and only use 100 GB, you’re paying for the full 500 GB.

Check allocated vs. used storage:

aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name FreeStorageSpace \
  --dimensions Name=DBInstanceIdentifier,Value=my-database \
  --start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 86400 \
  --statistics Minimum \
  --output text

Compare the FreeStorageSpace value against AllocatedStorage from describe-db-instances. If free space is consistently above 60%, your storage is overprovisioned.

Note: RDS does not support reducing allocated storage. You can only increase it. To reclaim overprovisioned storage, you need to create a new instance with the correct size and migrate data.

Migrate from Provisioned IOPS to gp3

This is where the big storage savings are. gp3 includes 3,000 IOPS and 125 MiB/s throughput at no extra cost. Provisioned IOPS (io1/io2) charges $0.065 per IOP with no free tier, which is 13x more expensive per IOP than gp3’s $0.005.

Storage cost comparison for 500 GB with 3,000 IOPS:

Storage TypeStorage CostIOPS CostTotal Monthly
gp3$40 (500 GB x $0.08)$0 (3,000 included)$40
io1$62.50 (500 GB x $0.125)$195 (3,000 x $0.065)$257.50

That’s an 84% savings for the same IOPS. For workloads that need up to 16,000 IOPS, gp3 is almost always the right choice. Only consider io1/io2 if you need sub-millisecond latency or more than 16,000 IOPS.

Modify storage type to gp3:

aws rds modify-db-instance \
  --db-instance-identifier my-database \
  --storage-type gp3 \
  --apply-immediately

The migration from gp2 or io1 to gp3 does not require downtime.

CostPatrol’s RDS-O003 rule checks for overprovisioned storage and instances still running on io1/io2 when gp3 would meet their IOPS requirements.

6. Multi-AZ: When It’s Worth It and When It’s Overkill

Multi-AZ doubles your compute and storage costs. For a db.r6g.large, that’s an extra $248/month. For a db.r6g.2xlarge, it’s an extra $496/month.

List all Multi-AZ instances:

aws rds describe-db-instances \
  --query 'DBInstances[?MultiAZ==`true`].{ID:DBInstanceIdentifier,Class:DBInstanceClass,Engine:Engine}' \
  --output table

When Multi-AZ is worth the cost:

  • Production databases where downtime directly impacts revenue
  • Databases with strict SLA requirements (99.95%+ uptime)
  • Applications that cannot tolerate 5-10 minutes of failover time

When Multi-AZ is overkill:

  • Development and staging environments
  • Batch processing databases that can tolerate brief outages
  • Databases behind applications with their own retry logic and degraded-mode behavior
  • Any database you could restore from snapshot within your RTO

Disabling Multi-AZ on a non-production db.r6g.large saves $248/month, or $2,976/year per instance.

Disable Multi-AZ:

aws rds modify-db-instance \
  --db-instance-identifier my-dev-database \
  --no-multi-az \
  --apply-immediately

7. Aurora vs. Standard RDS: The Cost Trade-Off

Aurora’s pricing looks similar to RDS at first glance, but the economics differ in important ways.

Instance costs: Aurora instances are 20-30% more expensive than equivalent standard RDS instances for the same class.

Storage costs: Aurora only charges for storage you actually use and auto-scales in 10 GB increments. Standard RDS charges for provisioned storage whether you use it or not. If your database size fluctuates significantly, Aurora’s pay-per-use model can be cheaper.

I/O costs: This is Aurora’s hidden variable. Aurora Standard charges per I/O request. For low-to-moderate I/O workloads, this is cheaper than provisioning IOPS on standard RDS. For I/O-heavy workloads, it can cost more. Aurora I/O-Optimized eliminates I/O charges but increases instance and storage rates.

When Aurora saves money:

  • Variable workloads where you’d overprovision standard RDS storage
  • Read-heavy workloads that benefit from Aurora’s up to 15 read replicas
  • Workloads with moderate I/O that would otherwise need io1/io2 on standard RDS

When standard RDS is cheaper:

  • Steady-state workloads where provisioned storage matches actual usage
  • High I/O workloads (Aurora’s per-I/O charges add up fast)
  • Small databases where Aurora’s higher instance cost isn’t offset by storage savings

Check your Aurora I/O costs (if using Aurora Standard):

aws ce get-cost-and-usage \
  --time-period Start=$(date -u -d '30 days ago' +%Y-%m-%d),End=$(date -u +%Y-%m-%d) \
  --granularity MONTHLY \
  --metrics BlendedCost \
  --filter '{"Dimensions":{"Key":"USAGE_TYPE","Values":["Aurora:StorageIOUsage"]}}' \
  --output table

If Aurora I/O costs exceed 25% of your total Aurora spend, evaluate switching to Aurora I/O-Optimized or migrating to standard RDS with gp3 storage.

8. Snapshot Cleanup and Backup Retention

RDS provides free backup storage equal to your total allocated database storage. Beyond that, you pay $0.095 per GB-month. Manual snapshots persist indefinitely until you delete them and always count against your paid backup storage.

List all manual snapshots sorted by size:

aws rds describe-db-snapshots \
  --snapshot-type manual \
  --query 'sort_by(DBSnapshots, &AllocatedStorage)[].{Snapshot:DBSnapshotIdentifier,DB:DBInstanceIdentifier,Size:AllocatedStorage,Created:SnapshotCreateTime}' \
  --output table

Find snapshots for databases that no longer exist:

aws rds describe-db-snapshots \
  --snapshot-type manual \
  --query 'DBSnapshots[].DBInstanceIdentifier' --output text | \
  tr '\t' '\n' | sort -u | while read db; do
    aws rds describe-db-instances \
      --db-instance-identifier "$db" 2>/dev/null || \
      echo "ORPHANED SNAPSHOTS: $db"
done

Orphaned snapshots for deleted databases are pure waste. Delete them after confirming you don’t need the data.

Review backup retention settings:

aws rds describe-db-instances \
  --query 'DBInstances[].{ID:DBInstanceIdentifier,RetentionDays:BackupRetentionPeriod}' \
  --output table

The default retention period is 7 days. Some teams set it to 35 days (the maximum) across the board without considering whether they actually need 35 days of point-in-time recovery. For non-production databases, 1-3 days is usually sufficient.

9. Read Replica Cost Analysis

Read replicas cost the same as standalone instances. A read replica of a db.r6g.large costs another $248/month. There’s no data transfer charge for replication within the same region, but cross-region replicas add $0.02/GB in transfer fees.

Before adding a read replica, check if your primary is actually read-constrained:

aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name CPUUtilization \
  --dimensions Name=DBInstanceIdentifier,Value=my-database \
  --start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 3600 \
  --statistics Average \
  --output table

If CPU is consistently below 30%, you don’t need a read replica. Optimize your queries or add a caching layer (ElastiCache, Redis) instead. A read replica at $248/month is expensive when query optimization or a $50/month cache node would solve the same problem.

Check existing read replica utilization:

aws rds describe-db-instances \
  --query 'DBInstances[?ReadReplicaSourceDBInstanceIdentifier!=null].{Replica:DBInstanceIdentifier,Source:ReadReplicaSourceDBInstanceIdentifier,Class:DBInstanceClass}' \
  --output table

Then check CPU and connection metrics on each replica. If a read replica is running below 20% CPU, either consolidate read traffic back to the primary or downsize the replica to a smaller instance class.

RDS Cost Optimization Checklist

Run through this list for every RDS instance in your account:

  • Idle check. Are connections and IOPS near zero for 7+ days? Delete or snapshot and delete.
  • Right-sizing. Is max CPU below 40% over 28 days? Drop one or two instance sizes.
  • Instance generation. Running db.m4, db.m5, db.r4, db.r5, or db.t2? Migrate to Graviton (m6g, r6g, m7g, r7g).
  • Reserved Instances. Running 24/7 for the foreseeable future? Buy a 1-year or 3-year RI after right-sizing.
  • Storage type. Using io1 or io2 with fewer than 16,000 IOPS? Switch to gp3.
  • Storage utilization. Is free space consistently above 60%? Plan a migration to a smaller allocation.
  • Multi-AZ. Enabled on non-production databases? Disable it.
  • Snapshots. Manual snapshots for deleted databases? Delete them.
  • Backup retention. Non-production databases set to 35 days? Reduce to 1-3 days.
  • Read replicas. Replica CPU below 20%? Consolidate or downsize.

Automate These Checks with CostPatrol

Running these commands manually works, but it doesn’t scale across dozens of instances and multiple AWS accounts. CostPatrol scans your RDS fleet automatically and flags:

  • RDS-O001: Idle instances with near-zero connections and I/O
  • RDS-O002: Previous-generation instances with upgrade recommendations
  • RDS-O003: Storage optimization opportunities (io1/io2 to gp3, overprovisioned allocations)

Each finding includes the estimated monthly savings and a remediation path. Instead of running CLI commands across every instance, you get a prioritized list of what to fix first.

See what CostPatrol finds in your AWS account

Free scan shows your total savings. Upgrade to Pro for full findings, fix commands, and daily Slack alerts.