plainstackBook a free call
← All guides

Databases12 min read

Why is my RDS bill so high?

It's the line people are most reluctant to touch, which is exactly why it accumulates the most untouched waste.

By Plainstack/

RDS is usually the second-largest line on an AWS bill, behind EC2, and it's the one people are most reluctant to touch. That reluctance is reasonable — it's the database — but it's also why RDS accumulates more untouched waste than anything else in the account.

The good news is that most of the waste is in configuration rather than in the database itself, so you can remove it without going anywhere near your data.

First, find out what you're actually paying for

The RDS line on your bill is at least five separate charges:

  • Instance hours — the compute, billed per second with a minimum, doubled if Multi-AZ is on.
  • Allocated storage — billed on what you provisioned, not what you're using.
  • Backup storage — free up to the size of your allocated storage, billed beyond that.
  • I/O — depending on engine and storage type. Aurora Standard bills I/O separately; gp3 and io1/io2 include or provision it differently.
  • Data transfer — mostly cross-AZ, which Multi-AZ and read replicas generate by design.

Before changing anything, split the RDS line by usage type in Cost Explorer. The fix for "instance hours dominate" is completely different from the fix for "backup storage dominates", and guessing wastes a week.

WHAT THE RDS LINE IS ACTUALLY MADE OFSINGLE-AZInstance hoursStorageBackupsMULTI-AZInstance hours × 2StorageBackupsThe standby instance you never query is billed at the same rate as the one you do.
Instance hours dominate an RDS bill before you change anything — and Multi-AZ doubles exactly that portion.

Multi-AZ is the big one

Multi-AZ roughly doubles the instance cost, because you're running a standby instance you never query.

For a production database serving customers, that's a fair price for automatic failover and it should stay on. The problem is that Multi-AZ is easy to enable, easy to copy when someone clones a configuration, and nobody ever revisits it. So it ends up on:

  • staging and development databases
  • internal admin tools
  • analytics replicas that can be rebuilt from source
  • the database behind a service that could be down for an hour with no customer impact

Turning it off in those places is an immediate, permanent halving of that instance's cost, and it changes nothing that anyone will notice.

The honest test is a single question: if this database failed over at 3am on a Sunday, who would have to be woken up? If the answer is nobody, you're paying for standby capacity you don't need.

Decide

Should this database keep Multi-AZ?

Customer-facing prodKeep it. Automatic failover is worth the doubled instance cost, and it also lets AWS do maintenance and minor version upgrades with far less downtime — often the better argument of the two.
Staging or devTurn it off. An hour of downtime on staging costs nothing, and you halve that instance immediately.
Internal toolsTurn it off unless the tool is genuinely operationally critical. Most admin dashboards and internal reporting databases are not.
Analytics replicaTurn it off if it can be rebuilt from the source. Rebuilding is slower than failing over, but it is also free.

If you're unsure, the safe order is: switch it off in non-production first, live with it for a month, then revisit production with real evidence rather than instinct.

One caveat worth knowing: for the production database, Multi-AZ also lets AWS perform maintenance and minor version upgrades with far less downtime. That operational benefit is real and is often the better argument for keeping it than the failover story.

The instance class is almost always too big

Databases get sized during a migration or a launch, based on a guess about peak load, and then never revisited. Two patterns show up repeatedly:

Memory-optimized classes for workloads that aren't memory-bound. The db.r family costs meaningfully more than db.m for the same vCPU count. It's the right choice when your working set needs to sit in RAM. It's a common default when nobody checked.

Sizing for a peak that never arrived. Look at CPU utilization, freeable memory, and database connections over at least a month that includes your busiest day. Sustained CPU under 20% with plenty of freeable memory means you're at least one size too large.

Also check whether Graviton applies. The db.m6g / db.r6g generation and later are cheaper than their Intel equivalents for equivalent performance on most workloads, and for a managed service like RDS the migration is a modify-instance operation rather than an application change. It's one of the least risky savings available.

Storage: the trap that can't be undone

Two things about RDS storage that surprise people:

You're billed for allocated storage, not used storage. A database with 1 TB allocated and 80 GB of actual data is billed for 1 TB.

You cannot shrink allocated storage. RDS lets you grow a volume. It does not let you shrink one. If someone provisioned generously years ago, the only route back is a dump and restore into a correctly sized instance — which is real work with real downtime, and needs to be planned rather than discovered.

Because of that, the practical advice is preventative: enable storage autoscaling and start small. Let it grow to meet demand rather than provisioning for three years out on day one.

If you're still on gp2, moving to gp3 is usually cheaper per gigabyte and decouples IOPS from volume size, so you stop over-provisioning storage purely to buy performance. See the EBS guide for the detail — the same logic applies here.

And if you're on io1 or io2 provisioned IOPS, verify you're actually consuming what you're paying for. Provisioned IOPS is expensive and is frequently chosen out of caution rather than measurement.

Backups and snapshots accumulate quietly

Automated backup storage is free up to the size of your allocated storage; beyond that you pay. Two things push you past it:

  • Retention set higher than your actual requirement. Thirty-five days of automated backups when your real recovery requirement is seven is a five-fold overspend on storage you will never restore from. Decide the number deliberately, write it down, apply it everywhere.
  • Manual snapshots, which live forever. Automated backups expire on schedule. Manual snapshots do not — they persist until someone deletes them, including snapshots of databases that were themselves deleted years ago. This is one of the most common sources of storage cost with no owner.

Also check Performance Insights retention. The default free tier covers seven days; longer retention is a paid feature that's easy to switch on and forget.

Read replicas nobody reads from

Read replicas are billed as full instances, plus the cross-AZ or cross-region data transfer to keep them in sync. They're created for a reporting need, or a migration, or a load concern that turned out not to materialise — and then they stay.

For each replica, check whether anything is actually connecting to it. If nothing is, it's a full-price instance plus ongoing transfer charges producing nothing.

Databases nobody uses at all

Look for instances with no connections over a period of weeks. Every account that has been running for a few years has at least one: the database behind a decommissioned service, a proof of concept, a migration source that was never cleaned up after cutover.

Take a final snapshot, verify you can restore it, then delete the instance. The snapshot costs a fraction of a running instance and keeps the safety net.

The same applies to Aurora Serverless v2 minimum capacity. A minimum ACU set above zero means you're paying continuously even when nothing is querying. For genuinely intermittent workloads, check whether the minimum can go lower — or to zero, where your engine version supports it.

Then, and only then, reserve

Once the instances are the right size and the unused ones are gone, buy Reserved Instances for RDS. Savings Plans do not cover RDS, and that gap is easy to leave open by accident — you buy a Compute Savings Plan, reasonably assume compute is handled, and the entire database estate stays at on-demand rates.

The ordering matters for the same reason it always does: reserve after right-sizing, or you lock in the oversized instance for a year. The commitments guide covers how to size that properly.

The order worth working in

  1. Split the RDS line by usage type so you know which problem you have.
  2. Turn off Multi-AZ everywhere it isn't genuinely needed.
  3. Delete idle databases and unused read replicas.
  4. Fix backup retention and sweep old manual snapshots.
  5. Right-size instance classes against a month of real metrics.
  6. Move to gp3, and to Graviton where available.
  7. Buy reservations for what's left.

Steps two, three and four are usually a single afternoon and produce most of the reduction. Step five needs measurement. Step seven should be last, always.

If you'd rather see how large the RDS share of your bill is before deciding whether any of this is worth your week, the free teardown reads a Cost Explorer export and tells you where RDS sits relative to everything else.

Check your own account

Does this apply to you? Find out in two seconds.

Drop a Cost Explorer export into the free teardown. It reads your service mix and tells you which of these patterns show up in your bill — in your browser, with no upload and no email.