Exam Room · Advanced Solutions Architect

How to Cut an RDS Bill Without Touching the Schema

October 03, 2026 · 26 min read

Advanced Cloud Architecture · part of The Exam Room

The situation

A logistics platform runs eleven RDS instances. Four are production PostgreSQL, Multi-AZ, on db.r5.4xlarge, each with 4 TB of gp2 storage. Three are read replicas of the busiest of those. The remaining four are staging and development copies, single-AZ, on db.r5.2xlarge, provisioned two years ago by a team that has since been reorganised out of existence.

The database line is AUD$41,000 a month and rising, against a total platform bill of AUD$126,000. Finance has asked for a number and a date. The application team has one condition: no schema changes, no query rewrites, and no maintenance window longer than the existing Sunday-morning patch slot.

That condition removes most of the usual advice. Indexes, query plans, and connection handling all sit on the far side of it. What remains is everything AWS charges for that the application never sees: the instance family, the storage type, the purchase commitment, the engine version, and the resources still running because nobody turned them off.

What actually matters

The first thing to establish is what the bill is actually made of, because “RDS” on an invoice is at least five separate charges. Instance hours, provisioned storage, provisioned IOPS, backup storage beyond the free allocation, and data transfer are billed independently, and they respond to completely different fixes. Right-sizing an instance does nothing to a bill dominated by 4 TB of over-provisioned gp2, and neither touches a snapshot library nobody has pruned since 2024.

The second is that a change with no application impact is worth more than a larger change that needs a negotiation. The team’s condition is not an obstacle to work around; it sorts the levers into the ones available this month and the ones that need a conversation. A storage type change that happens online is a different proposition from an engine upgrade that needs a window, even if the engine upgrade saves more.

The third is the distinction between reducing consumption and reducing the price of consumption. Turning off a development instance removes the cost entirely. Buying a Reserved Instance leaves the resource running and lowers its rate in exchange for a commitment. Both are legitimate, and doing them in the wrong order is expensive: commit to capacity first and you have locked in a discount on instances you were about to delete.

The fourth is that idle and over-provisioned are different diagnoses with different evidence. An instance at 8% CPU is over-provisioned and wants a smaller size. An instance at 0% for thirty days is abandoned and wants deleting. The metrics distinguish them, and treating the second as the first leaves a smaller wasted instance running forever.

Underneath all of it, some of this spend is on things AWS charges for that nobody chose. Extended support fees on an engine version past its standard support date, Performance Insights retention beyond the free window, and snapshots of instances that no longer exist all accrue without appearing in any architecture diagram.

What we’ll filter on

  1. Which charge does this lever reduce: instance hours, storage, IOPS, backup, or transfer?
  2. Does it need an outage, a maintenance window, or nothing at all?
  3. Does it reduce consumption or only the rate paid for it?
  4. Does it require the application to change, or is it invisible above the endpoint?
  5. Is it reversible if the workload turns out to need the capacity?
  6. Does it commit future spend?

The landscape

Right-sizing the instance. Compute Optimizer produces RDS instance recommendations from observed CloudWatch utilisation, and the evidence is usually stark on databases provisioned from a capacity guess rather than a measurement. A resize is applied in a maintenance window and takes a failover’s worth of downtime on Multi-AZ, which is seconds rather than minutes. It reduces instance hours only, and nothing else on the bill moves.

Moving to Graviton. The db.r6g and db.r7g families run on AWS Graviton processors at a lower hourly rate than their x86 equivalents, with better price-performance on most database workloads. For a managed engine the migration is a modify-instance operation, not a port, because nothing of yours is compiled against the processor. Engine version support is the constraint worth checking, and the change happens in a maintenance window like any other resize.

Changing the storage type. gp3 decouples IOPS and throughput from capacity, where gp2 ties performance to volume size and pushes teams into over-provisioning storage to buy performance. A 4 TB gp2 volume provisioned to get IOPS is the classic shape of this. Converting gp2 to gp3 happens online with no downtime, and if the volume was sized for performance rather than data, the capacity can then come down too. This one reduces both storage cost and, indirectly, the temptation to keep the instance large.

Reserved Instances. RDS capacity is discounted through Reserved Instances, on one- or three-year terms with all-upfront, partial-upfront, or no-upfront payment. The discount is substantial and the commitment is real: you pay for the term whether or not the instance runs. Savings Plans do not cover RDS. Compute Savings Plans apply to EC2, Lambda and Fargate, and a scenario offering a Savings Plan to discount a database is offering the wrong instrument.

Stopping or deleting non-production instances. A stopped RDS instance still bills for storage and backups but not for instance hours, and RDS restarts a stopped instance automatically after seven days, so “stopped” is not a durable state without automation. Deleting with a final snapshot is durable, cheap, and reversible at the cost of a restore. For development instances used during business hours, a scheduled stop and start removes roughly two thirds of the instance hours.

Aurora Serverless v2. For a workload with genuinely variable load, Aurora Serverless v2 scales capacity in fine-grained increments and bills per Aurora Capacity Unit-hour, with a configurable floor. It suits staging environments and spiky internal tools well. Moving a provisioned PostgreSQL instance to Aurora is a migration rather than a modify, so it sits on the far side of the team’s condition unless the target is a non-production copy.

Backup and snapshot hygiene. RDS gives backup storage equal to the total provisioned storage of the instance at no charge; beyond that it bills per GB-month. Manual snapshots persist after the instance is deleted and are a common source of spend nobody attributes to anything. Automated backup retention is a per-instance setting, and thirty-five days on a development copy is a decision nobody made.

Extended support. Running an engine version past the end of standard support incurs an Extended Support charge per vCPU-hour, which escalates over time. It is a real line on the bill for anyone who has deferred a major version upgrade, and the only fix is upgrading, which needs a window and application testing.

Performance Insights retention. The free tier covers seven days of performance history. Longer retention is billed per vCPU per month, and it is frequently enabled at the long retention across a whole fleet by an early decision nobody has revisited.

Evaluation

Side by side

Lever Reduces Downtime App change Reversible Commits spend
Right-size the instance Instance hours Failover
Move to Graviton Instance hours Failover
gp2 to gp3 Storage and IOPS ✗ none
Reserved Instances Rate on instance hours ✗ none ✓ 1 or 3 years
Schedule non-prod stop/start Instance hours By design
Delete abandoned instances Everything Permanent Via snapshot
Aurora Serverless v2 Instance hours Migration ✓ endpoint Hard
Prune snapshots and retention Backup storage ✗ none
Upgrade off Extended Support Extended Support fee Window ✓ testing Hard
Trim Performance Insights retention Per-vCPU fee ✗ none

Reading it against the team’s condition: everything above the Aurora row is available without a negotiation, and three of those need no downtime at all. The two levers that would need the application team, an Aurora migration and a major version upgrade, are also the two that are hardest to reverse, which is a reasonable argument for leaving both until the free wins have landed and the bill has been re-measured.

The solution

Sequence it: delete, then right-size, then change storage, then commit. Each step changes the input to the next, and running them in the wrong order buys a discount on capacity you were about to remove.

Start with the four staging and development instances, because the cheapest instance is the one that does not exist. Check thirty days of DatabaseConnections on each: an instance with no connections at all is abandoned, and a final snapshot plus a delete removes instance hours, storage, IOPS and backup in one action. Any that are genuinely used during business hours get an EventBridge schedule and a Systems Manager Automation runbook to stop them at 19:00 and start them at 07:00 on weekdays, which removes about two thirds of their hours. Set the automation to reassert the stop, since RDS restarts a stopped instance after seven days on its own.

Then right-size what remains, using Compute Optimizer’s recommendations rather than a guess, and take the Graviton family at the same time so one maintenance window buys both. Two changes, one failover per instance, applied in the existing Sunday slot. Watch CPUUtilization, FreeableMemory and ReadIOPS for a fortnight afterwards; a resize is reversible and the point of measuring is to know quickly if it was wrong.

Convert the gp2 volumes to gp3 while the instances run, because it needs no downtime and the 4 TB volumes are almost certainly sized for IOPS rather than data. Check actual consumed storage before deciding whether the capacity can shrink as well, and provision gp3 IOPS explicitly to whatever the gp2 volume was delivering, so performance is held constant while the price falls.

Only now buy Reserved Instances, against the instance types and sizes that survived the previous three steps. Buying earlier would have committed one to three years of spend on db.r5.4xlarge capacity that is about to become smaller Graviton capacity. Start with one-year no-upfront on the production instances, where the workload is stable and the commitment is defensible, and leave the read replicas on demand until their sizing has settled.

Alongside all of it, run the hygiene sweep, because it needs no window and no approval. List manual snapshots and delete the orphans; set automated backup retention to something deliberate per environment rather than whatever was inherited; and check Performance Insights retention across the fleet, keeping the long retention only where somebody actually reads it.

Why not lead with Reserved Instances. It is the largest single percentage on the page and the most tempting first move, and it locks the fleet’s current shape in for a year. Every other lever changes that shape.

Why not Aurora Serverless v2 for production. It suits variable load, and four production instances running steady traffic is the case it does not improve. It is worth revisiting for the staging copies once they are the only thing left.

Worked example

The thirty-day sweep of the four non-production instances finds two with zero connections for the full window, one used weekdays between 08:00 and 18:00, and one running a nightly job at 02:00. The two idle instances are snapshotted and deleted, removing AUD$3,400 a month. The weekday instance gets a stop-start schedule, removing about AUD$1,100. The nightly-job instance stays up, because a schedule that stops it would have to start it before 02:00 anyway and the saving is not worth the failure mode.

Compute Optimizer rates three of the four production instances as over-provisioned, recommending db.r6g.2xlarge against the current db.r5.4xlarge. Half the size and a Graviton family: the two changes together take those three instances from AUD$18,200 a month to about AUD$7,600. The fourth stays where it is, because its CPU sits above 60% at peak and the recommendation is to hold.

The storage conversion is the surprise. Consumed data across the four production volumes is 1.3 TB against 16 TB provisioned. The volumes were sized at 4 TB each to reach roughly 12,000 gp2 IOPS, which gp3 provides directly at 1 TB. Converting online and then reducing capacity takes the storage line from AUD$5,900 to about AUD$1,500, with IOPS provisioned explicitly so nothing gets slower.

The hygiene sweep finds 71 manual snapshots, 44 of them belonging to instances deleted more than a year ago, and Performance Insights on long retention across all eleven instances when two people have ever opened it.

Reserved Instances are then bought against db.r6g.2xlarge, which is the shape the fleet actually has rather than the shape it had six weeks earlier. The database line lands near AUD$16,000, and the only conversation with the application team was about a Sunday-morning window they were already having.

What’s worth remembering

  1. An RDS bill is at least five separate charges (instance hours, storage, IOPS, backup, transfer) and each responds to a different lever; right-sizing an instance does nothing about over-provisioned storage.
  2. Sequence matters more than any individual lever: delete, right-size, convert storage, and only then commit, because a Reserved Instance bought first discounts the fleet you are about to change.
  3. Savings Plans do not cover RDS. Databases are discounted through Reserved Instances, and any option offering a Savings Plan for RDS spend is a distractor.
  4. gp2 to gp3 is the free win: it happens online with no downtime, and it breaks the link between volume size and IOPS that pushed the volume to 4 TB in the first place.
  5. A stopped RDS instance still bills for storage and backups, and RDS restarts it automatically after seven days, so scheduled stop-start needs automation that reasserts itself.
  6. Extended Support fees, long Performance Insights retention, and manual snapshots of deleted instances are charges nobody chose and nobody can see on an architecture diagram.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.