RDS Multi-AZ on Non-Production Instance
What does ZopNight detect here?
Multi-AZ enabled on non-production RDS instances doubles compute, storage, IOPS, and throughput for a standby dev workloads rarely justify. ZopNight prices the saving as exactly 0.50 of that Multi-AZ-attributable component basis, clamped to half the monthly bill, suppressing when a reserved instance keeps billing regardless and abstaining on cluster members.
Signal and threshold
| Field | Value |
|---|---|
| Rule IDs | RC-106 |
| Category | rightsizing |
| Severity | low |
| Metric | none — pure configuration read |
| Source | rds_multiaz_nonprod.go |
Where it applies
Why cluster members and covered instances are skipped
- resource.Status: statusAvailable
- env classification (EV-T0068): prod env-tag is an authoritative veto; fires when non-prod by name or by a dev/test env-tag, not explicitly prod-tagged
- Metadata[“multi_az”] (JSON bool, discoverer-stamped, and not a Tag; the prior hasTag gate was dead) must be true
- Not a cluster member (resource.ParentUID == ""): the ÷2 “remove one standby” saving model applies only to a classic single-standby Multi-AZ instance. A Multi-AZ DB cluster is a ~3× two-readable-standby topology priced at the cluster level (#2251), so RC-106 abstains on any cluster member. AWS already reports DBInstance.MultiAZ=false for cluster members (HA is a cluster property signalled by DBClusterIdentifier→ParentUID), so the multi_az gate normally suffices; the ParentUID guard is defense-in-depth so the rec can never mis-fire even if that flag were set.
- PricingAware: full MonthlyCost (CurrentCostUSD display basis + defensive clamp; no longer the savings basis, see Cost calculation)
- RDSMultiAZAttributableCostAware: the per-resource monthly sum of the calcRDS components Multi-AZ doubles (compute+storage+iops+throughput), which is the savings basis
- SetReservationCoverage: per-resource purchase type, which suppresses the rec when it is Reservation or SavingsPlan. An RDS RI is a sunk commitment that keeps billing whether or not Multi-AZ is enabled (it can’t be turned off), so disabling Multi-AZ yields no realizable saving on a covered instance until it expires. Fires on OnDemand/unknown coverage (fail-open, mirroring ec2_heatmap_schedule). Sourced from the aggregator’s CUR-derived ComputedCost.PurchaseType (billing-connected orgs).
Halving only the four components Multi-AZ doubles
the standby saving is priced off the Multi-AZ-attributable component basis alone, never a fraction of the whole bill. SetMultiAZAttributableCost injects basis[uid] = the monthly sum of the calcRDS compute+storage+iops+throughput CostComponents (aggregator’s ComputeResourceCosts → ComputedCost.Components, parsed and daily→monthly projected ×30 by fetchRDSMultiAZAttributableCost in service.go, mirroring fetchLambdaGBSecondCost). calcRDS doubles exactly those four components under Multi-AZ, the same four AWS charges twice for a synchronous standby (calc_aws_database.go: one shared azMultiplier applied to all four). Backup beyond the free tier and data transfer are not doubled, and calcRDS doesn’t model them. So savings = basis × rdsMultiAZStandbyShare (0.50) is exact on the rack-rate (calculated) path and on any billing-actual bill priced at/near rack: it prices only the doubled components and excludes the non-doubled line items (backup, data transfer) a real bill also carries, which a whole-bill×0.5 previously (and wrongly) folded in and overstated. Clamped to half the displayed cost (cost × 0.50, Pricing[uid]): the synchronous standby is structurally ≤ half the total bill (one of two identical instances + its attached storage). On a deeply discounted billing-actual bill (EDP/private rate, PurchaseType still OnDemand so the RI/SP suppression doesn’t fire) the rack basis/2 can exceed the discounted bill’s standby half; the clamp then caps the saving at cost × 0.50, a tight upper bound rather than an exact figure in that case, since cost also carries non-doubled backup/data-transfer, so cost×0.5 can overstate the true removable standby by at most half of those (typically ~0: backups within DB size are free, DB cross-AZ transfer is small). We accept that bounded, rarely-material overshoot rather than abstain on every discounted org (no discounted-component breakdown exists to price the standby exactly). The rule abstains (returns nil) when either Pricing[uid] or multiAZAttributableCost[uid] is absent or ≤ 0, and it never falls back to a fraction of the whole bill.
- Aggregator wiring: ComputeResourceCosts (aggregator/internal/service/service.go) computes the rack-rate cost.Calculate output unconditionally (both branches), so output.Components is available even when the headline MonthlyCost comes from real billing. The billing-actual branch now also marshals output.Components onto the returned ComputedCost (previously it carried through HourlyRate but dropped Components, which would have made this basis silently absent, and RC-106 permanently abstain, for every billing-connected org).
- Re-added plumbing: a 2026-07-13 pass (03fb6105d) deleted the prior compute-only ComputeCostAware/fetchRDSComputeCost seam and widened RC-106 to MonthlyCost×0.50. This change supersedes that: a new RDSMultiAZAttributableCostAware interface (rule.go) + fetchRDSMultiAZAttributableCost producer (service.go, wired at the same two HandleDiscovered/refreshRecommendationsSync injection sites as SetPricing) sum all four doubled components (not just compute), fixing the whole-bill overstatement on the billing-actual path while staying exact on the calculated path.
- Basis accuracy on the gp3 striped baseline: because the standby saving is derived from calcRDS’s iops/throughput components, calcRDS uses AWS’s size/engine-aware gp3 free baseline (gp3Baselines): 3,000 IOPS / 125 MiBps, rising to 12,000 / 500 for striped volumes (≥400 GiB; ≥200 GiB Oracle; SQL Server never stripes). A flat 3,000/125 would have billed IOPS/throughput AWS provides for free on large gp3 volumes, inflating the component basis and, halved, the reported RC-106 saving. See aggregator docs/COST.md → RDS Instances.
Turning off the standby in place
- Verify this is a non-production database
- Go to RDS Console → Modify → Disable Multi-AZ (rds:ModifyDBInstance, MultiAZ=false); hot in-place, no stop/reboot
- Apply during maintenance window
- Saving is half the instance’s monthly cost: the standby replica’s compute, storage, IOPS, and throughput are all removed together