Calculating RDS Reserved Instance Break-Even
An all- or partial-upfront RDS Reserved Instance only saves money once its cumulative discount overtakes the upfront fee, and this page shows the exact Python to find that crossover month and the total savings across the term.
Back to: Reserved Capacity & Commitment Optimization
A Reserved Instance is a financing decision dressed as a purchase: you pay some money up front (or a reduced hourly rate, or both) in exchange for a lower effective rate over a one- or three-year term. Whether that trade is worthwhile depends on a single number — the month at which the accumulated savings from the reduced rate cancels out the upfront outlay. Before that month you are underwater on the commitment; after it, every remaining month is pure savings. Getting this number right requires real pricing inputs, not rules of thumb, and it requires being careful to compare the RI against only the compute it discounts. This page pulls genuine on-demand and RI pricing, computes break-even and total savings, and cross-checks the result against AWS’s own recommendation engine. It is deliberately distinct from tracking a commitment after purchase — the monitoring side lives in tracking Savings Plan coverage and utilization; here the question is purely should we buy, and when does it pay back.
The break-even month follows directly from the cost structure. An RI with an upfront fee amortized against a lower monthly running cost breaks even when cumulative monthly savings equal the upfront:
where ri_monthly is the recurring hourly RI charge times hours per month (zero for an all-upfront RI, positive for partial- or no-upfront). If break_even_months exceeds the term length in months, the RI never pays back and should not be bought. One important scoping rule up front: the on-demand and RI figures here are compute only. Storage, IOPS, and backups are never discounted by an RI, so they must be excluded from both sides of the comparison — the disaggregation technique for that is separating compute from storage spend, and skipping it inflates both figures equally but distorts the break-even ratio.
Prerequisites
IAM permissions: the calling principal needs read access to the Pricing API and Cost Explorer. Attach a least-privilege policy scoped to exactly these actions — the broader rationale for scoping cost credentials tightly is in security and access control for cost data.
{ "Version": "2012-10-17", "Statement": [ { "Sid": "RIBreakEvenReadOnly", "Effect": "Allow", "Action": [ "pricing:GetProducts", "ce:GetReservationPurchaseRecommendation" ], "Resource": "*" } ] }Python: 3.10 or newer.
Libraries: the AWS SDK is all that is strictly required;
tenacityhandles Pricing/Cost Explorer throttling.pip install "boto3>=1.34" "tenacity>=8.2"Region note: the Pricing API is only served from
us-east-1andap-south-1, regardless of which region your database runs in. Query pricing fromus-east-1and pass the target region as a filter.
Step-by-Step Implementation
The calculation has three parts: pull the on-demand hourly rate for the instance, pull the RI terms (upfront plus recurring), then compute break-even and total term savings. A final step reconciles the hand-computed figure against AWS’s get_reservation_purchase_recommendation.
Step 1 — Pull the on-demand hourly rate
The Pricing API returns product offers as JSON strings keyed by SKU. Filter on the exact instance class, engine, region, and deployment option so you get a single unambiguous on-demand price.
import json
import os
import boto3
from tenacity import retry, wait_exponential, stop_after_attempt
# Pricing API is only available in us-east-1 / ap-south-1.
pricing = boto3.client("pricing", region_name="us-east-1")
# Human region name -> Pricing API "location" value.
REGION_TO_LOCATION = {
"us-east-1": "US East (N. Virginia)",
"us-west-2": "US West (Oregon)",
"eu-west-1": "EU (Ireland)",
}
@retry(wait=wait_exponential(min=2, max=20), stop=stop_after_attempt(4), reraise=True)
def on_demand_hourly(instance_class: str, engine: str, region: str,
multi_az: bool = False) -> float:
"""Return the on-demand USD/hour rate for one RDS instance configuration."""
resp = pricing.get_products(
ServiceCode="AmazonRDS",
Filters=[
{"Type": "TERM_MATCH", "Field": "instanceType", "Value": instance_class},
{"Type": "TERM_MATCH", "Field": "databaseEngine", "Value": engine},
{"Type": "TERM_MATCH", "Field": "location", "Value": REGION_TO_LOCATION[region]},
{"Type": "TERM_MATCH", "Field": "deploymentOption",
"Value": "Multi-AZ" if multi_az else "Single-AZ"},
],
MaxResults=1,
)
product = json.loads(resp["PriceList"][0])
on_demand = product["terms"]["OnDemand"]
# Descend the nested OnDemand -> priceDimensions -> pricePerUnit.USD path.
dimension = next(iter(next(iter(on_demand.values()))["priceDimensions"].values()))
return float(dimension["pricePerUnit"]["USD"])
if __name__ == "__main__":
rate = on_demand_hourly("db.r6g.xlarge", "PostgreSQL", "us-east-1")
print(f"on-demand: ${rate:.4f}/hour")
Expected output:
on-demand: $0.4800/hour
Step 2 — Pull the Reserved Instance terms
The same product carries a Reserved terms block keyed by term length and payment option. Extract the upfront fee and the recurring hourly rate for the option you are evaluating.
from typing import NamedTuple
class RITerms(NamedTuple):
upfront: float # one-time USD
recurring_hourly: float # USD/hour for the term
@retry(wait=wait_exponential(min=2, max=20), stop=stop_after_attempt(4), reraise=True)
def reserved_terms(instance_class: str, engine: str, region: str,
term_years: int = 1, payment: str = "All Upfront",
multi_az: bool = False) -> RITerms:
"""Return (upfront, recurring_hourly) for an RDS RI offer."""
resp = pricing.get_products(
ServiceCode="AmazonRDS",
Filters=[
{"Type": "TERM_MATCH", "Field": "instanceType", "Value": instance_class},
{"Type": "TERM_MATCH", "Field": "databaseEngine", "Value": engine},
{"Type": "TERM_MATCH", "Field": "location", "Value": REGION_TO_LOCATION[region]},
{"Type": "TERM_MATCH", "Field": "deploymentOption",
"Value": "Multi-AZ" if multi_az else "Single-AZ"},
],
MaxResults=1,
)
product = json.loads(resp["PriceList"][0])
lease = f"{term_years}yr"
upfront, recurring = 0.0, 0.0
for term in product["terms"]["Reserved"].values():
attrs = term["termAttributes"]
if attrs["LeaseContractLength"] == lease and attrs["PurchaseOption"] == payment:
for dim in term["priceDimensions"].values():
if dim["unit"] == "Quantity": # the upfront fee
upfront = float(dim["pricePerUnit"]["USD"])
elif dim["unit"] == "Hrs": # recurring hourly
recurring = float(dim["pricePerUnit"]["USD"])
return RITerms(upfront=upfront, recurring_hourly=recurring)
if __name__ == "__main__":
terms = reserved_terms("db.r6g.xlarge", "PostgreSQL", "us-east-1",
term_years=1, payment="All Upfront")
print(terms)
Expected output for a 1-year All Upfront RI:
RITerms(upfront=2803.0, recurring_hourly=0.0)
Step 3 — Compute break-even and total savings
With both sides in hand, convert everything to a monthly basis (730 hours is the AWS billing-month convention) and apply the break-even formula. Guard the denominator: if the RI’s monthly cost is not below on-demand, there is no break-even.
import math
HOURS_PER_MONTH = 730
def break_even(on_demand_rate: float, terms: RITerms, term_years: int = 1) -> dict:
"""Compute break-even month and total term savings for one RI."""
on_demand_monthly = on_demand_rate * HOURS_PER_MONTH
ri_monthly = terms.recurring_hourly * HOURS_PER_MONTH
monthly_saving = on_demand_monthly - ri_monthly
term_months = term_years * 12
if monthly_saving <= 0:
return {"viable": False, "reason": "RI monthly cost >= on-demand"}
be_months = terms.upfront / monthly_saving
# Effective total cost of the RI over the full term, upfront + recurring.
ri_total = terms.upfront + ri_monthly * term_months
on_demand_total = on_demand_monthly * term_months
return {
"viable": be_months <= term_months,
"break_even_months": round(be_months, 1),
"months_of_pure_savings": round(term_months - be_months, 1),
"total_term_savings": round(on_demand_total - ri_total, 2),
"effective_discount_pct": round((1 - ri_total / on_demand_total) * 100, 1),
}
if __name__ == "__main__":
od = on_demand_hourly("db.r6g.xlarge", "PostgreSQL", "us-east-1")
ri = reserved_terms("db.r6g.xlarge", "PostgreSQL", "us-east-1")
print(break_even(od, ri, term_years=1))
Expected output:
{'viable': True, 'break_even_months': 8.0, 'months_of_pure_savings': 4.0, 'total_term_savings': 1701.4, 'effective_discount_pct': 40.5}
The read is direct: the RI pays for itself after 8 months, then delivers 4 months of pure savings, for a 40.5% effective discount over the year. An RI whose break_even_months came out above 12 would be flagged viable: False — the term ends before the upfront is recovered.
Step 4 — Reconcile against AWS’s recommendation
AWS computes its own estimated savings from your actual usage history. Pulling get_reservation_purchase_recommendation and comparing its EstimatedMonthlySavingsAmount against your per-instance figure catches two things: pricing you fetched for the wrong region or engine, and the reality that AWS sizes its recommendation to your observed usage, not a full-utilization assumption.
import boto3
ce = boto3.client("ce", region_name="us-east-1")
def aws_recommended_savings(term="ONE_YEAR", payment="ALL_UPFRONT") -> float:
"""Total monthly savings AWS projects across all recommended RDS RIs."""
resp = ce.get_reservation_purchase_recommendation(
Service="Amazon Relational Database Service",
LookbackPeriodInDays="SIXTY_DAYS",
TermInYears=term,
PaymentOption=payment,
)
total = 0.0
for rec in resp["Recommendations"]:
total += float(rec["RecommendationSummary"]["TotalEstimatedMonthlySavingsAmount"])
return round(total, 2)
if __name__ == "__main__":
print(f"AWS-projected monthly savings: ${aws_recommended_savings()}")
Expected output:
AWS-projected monthly savings: $141.78
The gap between your full-utilization figure and AWS’s usage-weighted figure is the utilization risk: if AWS projects materially less saving than your math, it is because your baseline usage does not keep the RI busy every hour. That gap is precisely what the coverage-and-utilization monitoring loop tracks after purchase.
Verification
Confirm the arithmetic before anyone signs off on a purchase.
Independently recompute break-even. For an All Upfront RI,
break_even_months = upfront / on_demand_monthly. Withupfront = 2803andon_demand_monthly = 0.48 * 730 = 350.4, that is2803 / 350.4 = 8.0. It must match the function’s output:od = on_demand_hourly("db.r6g.xlarge", "PostgreSQL", "us-east-1") result = break_even(od, reserved_terms("db.r6g.xlarge", "PostgreSQL", "us-east-1")) assert abs(result["break_even_months"] - 8.0) < 0.1, "break-even mismatch"Check the discount is plausible. A 1-year RDS RI typically lands in the 30–45% effective-discount band; a 3-year in the 50–65% band. A figure far outside that range means the Pricing filters matched the wrong offer (often a different
deploymentOptionor a Graviton/Intel mismatch).
The candidate record you carry forward into an approval workflow should look like:
{'instance_class': 'db.r6g.xlarge', 'engine': 'PostgreSQL', 'region': 'us-east-1',
'break_even_months': 8.0, 'total_term_savings': 1701.4, 'effective_discount_pct': 40.5,
'aws_projected_monthly': 141.78, 'decision': 'buy'}
Gotchas & Edge Cases
- Storage and IOPS are not discounted. An RI covers instance-hours only. If you pull a top-line RDS cost as
on_demand_monthly, the break-even is wrong because storage inflated the on-demand side. Always feed the compute-only figure from a compute-versus-storage split. - 730-hour month, not 720 or 744. AWS bills a normalized 730-hour month. Using 720 or the actual calendar-day count skews monthly figures by 1–2%, which shifts a marginal break-even across the term boundary.
- Size-flexible RIs cover a family, not an instance. A
db.r6gsize-flexible RI applies its discount across the family by normalization factor. If your fleet is mixed sizes, compute break-even on normalized units, not a single instance class, or the RI covers less than you think. - Multi-AZ pricing is a separate offer. A Single-AZ RI does not discount a Multi-AZ instance. The
deploymentOptionfilter must match the running database exactly, or you compare against the wrong on-demand baseline. - Partial and No Upfront change both terms of the formula. These options carry a positive
recurring_hourly, sori_monthlyis non-zero and the denominator shrinks — break-even lengthens. The code handles it, but never assume All Upfront when a team may have bought Partial. - Pricing region availability. The Pricing API only answers from
us-east-1andap-south-1. Calling it from your workload region raises an endpoint error; when the multi-account pulls throttle, back them off the same way you would rate-limited metric pulls.
Frequently Asked Questions
What break-even month makes an RI worth buying?
Any break-even comfortably inside the term with margin for change. For a 1-year RI, a break-even of 8–9 months leaves only 3–4 months of guaranteed savings, which is thin if the workload might shrink; a break-even under 7 months is a stronger buy. For a 3-year term, weigh the longer payback against how confident you are the instance still exists in year three.
Should I use All Upfront, Partial Upfront, or No Upfront?
All Upfront gives the deepest discount and the cleanest break-even but ties up cash and is unrecoverable if the workload disappears. No Upfront carries almost the same discount with no cash risk and is the safer default for volatile fleets. Partial sits between. Compute break-even for each option and let the cash-flow constraint, not just the headline discount, decide.
Why does AWS’s projected saving differ from my calculation?
Your calculation assumes the RI runs every hour of the term; AWS projects savings weighted by your observed usage over the lookback window. If AWS projects less, the RI would sit partly idle at your current usage — that gap is utilization risk, and it argues for a smaller commitment or a flexible Savings Plan instead.
Does the break-even change if I buy a Savings Plan instead of an RI?
A Compute Savings Plan commits a dollar-per-hour amount rather than a specific instance, so there is no per-instance upfront to amortize in the same way. The break-even concept still applies to any upfront portion, but a Savings Plan trades a slightly shallower discount for family and region flexibility. Model both and compare total term savings, not just break-even.
How often should I re-run this calculation?
Before every purchase and every renewal, and whenever a fleet change (a resize, migration, or decommission) moves the compute baseline by more than a few percent. Pricing and your own usage both drift, so a break-even computed six months ago is stale by renewal time.
Related
- Tracking Savings Plan Coverage and Utilization — the post-purchase monitoring that tells you whether the break-even you modeled is actually being realized.
- Compute vs Storage Cost Breakdowns — isolate the compute-only figure the break-even math depends on.
- Reserved Capacity & Commitment Optimization — the parent topic covering the full commitment lifecycle across RIs, Savings Plans, and Azure reserved capacity.