Commitment Discount Optimization: Savings Plans & RIs

Commitment discount optimization is the discipline of turning three providers’ utilization and coverage telemetry — AWS Savings Plans and Reserved Instances, GCP Committed Use Discounts, and Azure Reservations — into a single, decision-ready signal: is this commitment paying for itself, and should you buy more, less, or nothing at all. The engineering problem is not the purchase decision itself, it is the data pipeline underneath it: GetSavingsPlansUtilization, GetSavingsPlansCoverage, and GetReservationUtilization each return a different denominator, throttle independently, and paginate on their own cadence, while GCP and Azure expose the equivalent telemetry through entirely different surfaces (a nested credits array in the billing export versus a dedicated Consumption API). This page sits in the allocation/normalization stage of the FinOps Architecture & Billing Fundamentals pipeline: it consumes the coverage matrix already resolved by Reserved Instance Mapping Logic and turns it into three scored outputs — effective savings rate, break-even period, and a recommendation score — that a purchasing decision can actually act on. Get the query construction and pagination wrong here and the numbers that drive six- and seven-figure commitment purchases are simply not trustworthy.

Architecture Context & Data-Flow Position

Commitment discount optimization occupies the same position in the pipeline as the mapping engine it sits beside: the boundary between normalized usage data and the allocation layer that distributes cost to business units. Where Reserved Instance Mapping Logic answers which commitment covered which hour, this page answers the financial question layered on top — is the commitment portfolio, taken as a whole, an efficient use of capital. It pulls aggregate utilization and coverage directly from each provider’s cost API rather than re-deriving it from raw usage rows, because the vendor-reported percentages are the number finance reconciles against the invoice, and any internal figure that drifts from it erodes trust in the pipeline. The distinction between coverage and utilization as two structurally different ratios — and why you cannot derive one from the other — is formalized in Reserved Instance Coverage vs Utilization Metrics; this page assumes that reconciliation is solved and focuses on turning the resulting metrics into a purchase-grade recommendation score.

Commitment discount data pipeline: three provider APIs into a normalization and scoring engine A convergence diagram with three source boxes at the top. AWS Cost Explorer returns GetSavingsPlansUtilization, GetSavingsPlansCoverage, GetReservationUtilization, and GetReservationCoverage. GCP exposes the billing export credits array plus the Recommender API's CommittedUseDiscountRecommender. Azure exposes the Consumption API's reservationSummaries, reservationDetails, and reservationRecommendations endpoints. Arrows from all three converge into a central normalization and scoring engine that computes effective savings rate, break-even days, and a coverage gap score, with a dashed retry loop labeled retry on throttling errors above the AWS box. A final arrow flows down into the allocation and amortization stage that consumes the scored output. AWS Cost Explorer GCP Export + Recommender Azure Consumption API GetSavingsPlansUtilization GetSavingsPlansCoverage GetReservationUtilization GetReservationCoverage credits[] CUD amount CommittedUseDiscount- Recommender reservationSummaries reservationDetails reservationRecommendations retry on ThrottlingException Normalization & Scoring Engine effective savings rate · break-even days coverage gap score · recommendation rank Allocation & Amortization Stage tag-based showback · amortized commitment cost
Three provider APIs feed a normalization and scoring engine that computes effective savings rate, break-even, and a recommendation score, which then hands off to the allocation and amortization stage.

Each provider’s telemetry surface has a distinct shape, and the engine below has to query each one on its own terms before normalizing the result. The table lists the operations this page’s engine calls, built on the same Cost Explorer client covered in AWS Cost Explorer Architecture:

Provider API / Operation Returns Interval / Lookback
AWS GetSavingsPlansUtilization Utilization % and unused commitment per Savings Plan DAILY / MONTHLY; up to 13 months
AWS GetSavingsPlansCoverage On-demand-equivalent coverage % of eligible spend DAILY / MONTHLY; up to 13 months
AWS GetReservationUtilization RI utilization % and hours consumed vs purchased DAILY / MONTHLY; up to 13 months
AWS GetReservationCoverage RI coverage % of eligible on-demand-equivalent usage DAILY / MONTHLY; up to 13 months
AWS GetSavingsPlansPurchaseRecommendation Recommended hourly commitment and estimated savings 7 / 30 / 60-day lookback
GCP BigQuery export credits[] (type = COMMITTED_USAGE_DISCOUNT) Discount amount applied per SKU row Per billing row, continuous
GCP Recommender API CommittedUseDiscountRecommender Recommended CUD purchase and projected savings Refreshed roughly every 24h
Azure Consumption API reservationSummaries Utilization % per reservation, daily or monthly grain Daily / Monthly
Azure Consumption API reservationDetails Per-resource reservation consumption records Daily; max 3-month range per call
Azure Consumption API reservationRecommendations Recommended reservation purchase and estimated savings Continuous, scope-dependent

Core Implementation Patterns

1. Least-Privilege IAM Across Three Providers

The scoring engine only ever reads billing and recommendation data — it never touches compute, storage, or purchasing APIs directly, even when it surfaces a recommendation for a human to act on. On AWS, scope the role to the Cost Explorer read actions and run it from the management (payer) account, because linked accounts cannot see organization-wide Savings Plans utilization:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "ce:GetSavingsPlansUtilization",
      "ce:GetSavingsPlansCoverage",
      "ce:GetSavingsPlansPurchaseRecommendation",
      "ce:GetReservationUtilization",
      "ce:GetReservationCoverage"
    ],
    "Resource": "*"
  }]
}

On GCP, grant roles/billing.viewer on the billing account for the export-derived credit data and roles/recommender.cudViewer (or the broader roles/recommender.viewer) for CUD recommendations — never roles/billing.admin, which the engine has no need for. On Azure, the built-in Reservations Reader role at the reservation-order scope, plus Cost Management Reader at the subscription or billing-account scope for reservationDetails, is sufficient; avoid granting Reservations Purchaser, which this engine never invokes.

2. Coverage vs. Utilization Query Construction

Coverage and utilization answer different questions and must be requested as separate calls with separate GroupBy dimensions — merging their raw responses on mismatched keys is the most common source of bad recommendation scores, as detailed in Reserved Instance Coverage vs Utilization Metrics. For Savings Plans specifically, the same asymmetry holds between GetSavingsPlansUtilization (denominator = purchased commitment) and GetSavingsPlansCoverage (denominator = total eligible on-demand-equivalent spend):

response = ce_client.get_savings_plans_utilization(
    TimePeriod={"Start": "2026-06-01", "End": "2026-07-01"},
    Granularity="DAILY",
    Filter={
        "Dimensions": {
            "Key": "SAVINGS_PLAN_ARN",
            "Values": [savings_plan_arn],
        }
    },
)

GetSavingsPlansCoverage takes the same TimePeriod/Granularity shape but returns SpendCoveredBySavingsPlans against OnDemandCostEquivalent rather than commitment hours — treat the two responses as separate datasets joined later on (time_period, savings_plan_arn or instance_family), never on raw row order.

3. Pagination and Rate-Limit Handling

Every Cost Explorer utilization and coverage call can return a NextPageToken, and a naive single-page read silently truncates results once a Savings Plan or reservation portfolio spans more than the response’s row cap (roughly 200–1000 rows per page depending on granularity and grouping). Loop until the token is empty, and back off on ThrottlingException rather than retrying immediately:

def paginate(fn, **kwargs):
    token = None
    while True:
        if token:
            kwargs["NextPageToken"] = token
        resp = fn(**kwargs)
        yield resp
        token = resp.get("NextPageToken")
        if not token:
            break

Azure’s reservationDetails imposes a hard 3-month window per call, so a 12-month lookback requires four sequential windowed calls stitched together client-side rather than one wide query — there is no server-side pagination token for this endpoint, only date-range chunking.

4. Effective Savings Rate and Break-Even Scoring

Once utilization and coverage are normalized, the engine derives two purchase-grade numbers. Effective savings rate is the blended discount actually realized: (on_demand_equivalent_cost − net_commitment_cost) / on_demand_equivalent_cost, computed from the coverage response’s OnDemandCostEquivalent against the amortized commitment spend. Break-even period is how many days of the term must elapse, at the current utilization rate, before the commitment’s upfront and recurring cost is offset by the discount already realized — a Savings Plan running at 60% utilization with a 92% effective rate on covered spend breaks even far later than the headline discount percentage implies, which is why utilization and the discount rate must be scored together, never separately.

Production-Grade Python Commitment Scoring Engine

The module below pulls Savings Plans and Reserved Instance utilization and coverage from AWS Cost Explorer, paginates both feeds, and computes effective savings rate, break-even days, and a composite recommendation score per commitment. It uses tenacity for throttling backoff, @dataclass models for typed records, structured logging, and a __main__ guard that runs a 30-day scoring pass. Dependencies: boto3>=1.34.0, tenacity>=8.2.0.

import logging
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from decimal import Decimal, ROUND_HALF_UP
from typing import Dict, Iterator, List, Optional

import boto3
from botocore.exceptions import ClientError
from tenacity import (
    retry,
    retry_if_exception_type,
    stop_after_attempt,
    wait_exponential,
)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logger = logging.getLogger("commitment.scoring")

# Cost Explorer throttles around 5 req/s per account; only retry the
# throttling/limit-exceeded class, not permission or validation errors.
THROTTLE_CODES = {"ThrottlingException", "TooManyRequestsException", "LimitExceededException"}


def is_throttle(exc: Exception) -> bool:
    return isinstance(exc, ClientError) and exc.response["Error"]["Code"] in THROTTLE_CODES


@dataclass(frozen=True)
class UtilizationRecord:
    """One period's utilization for a single Savings Plan or RI commitment."""

    commitment_id: str
    period_start: date
    period_end: date
    utilization_pct: Decimal
    unused_commitment: Decimal
    total_commitment: Decimal


@dataclass(frozen=True)
class CoverageRecord:
    """One period's coverage for a commitment family/dimension."""

    dimension_key: str
    period_start: date
    period_end: date
    coverage_pct: Decimal
    on_demand_equivalent_cost: Decimal
    covered_spend: Decimal


@dataclass
class CommitmentScore:
    """The composite, purchase-grade score the pipeline emits per commitment."""

    commitment_id: str
    utilization_pct: Decimal
    coverage_pct: Decimal
    effective_savings_rate: Decimal
    break_even_days: Optional[int]
    recommendation_score: Decimal
    flags: List[str] = field(default_factory=list)


class CommitmentDiscountEngine:
    """Pulls AWS Savings Plans and RI utilization/coverage and scores commitments."""

    def __init__(self, ce_client=None, lookback_days: int = 30) -> None:
        self.ce = ce_client or boto3.client("ce")
        self.lookback_days = lookback_days

    def _time_period(self) -> Dict[str, str]:
        end = datetime.utcnow().date()
        start = end - timedelta(days=self.lookback_days)
        return {"Start": start.isoformat(), "End": end.isoformat()}

    @retry(
        stop=stop_after_attempt(6),
        wait=wait_exponential(multiplier=1, min=2, max=32),
        retry=retry_if_exception_type(ClientError),
    )
    def _call(self, fn, **kwargs):
        try:
            return fn(**kwargs)
        except ClientError as exc:
            if is_throttle(exc):
                logger.warning("throttled calling %s; backing off", fn.__name__)
                raise
            # Non-throttle ClientErrors (auth, validation) fail fast — retrying
            # them wastes the retry budget on an error that will never resolve.
            logger.error("non-retryable error calling %s: %s", fn.__name__, exc)
            raise

    def _paginate(self, fn, **kwargs) -> Iterator[dict]:
        """Loop NextPageToken until AWS stops returning one."""
        token = None
        while True:
            call_kwargs = dict(kwargs)
            if token:
                call_kwargs["NextPageToken"] = token
            resp = self._call(fn, **call_kwargs)
            yield resp
            token = resp.get("NextPageToken")
            if not token:
                break

    def fetch_savings_plans_utilization(self) -> List[UtilizationRecord]:
        records: List[UtilizationRecord] = []
        for page in self._paginate(
            self.ce.get_savings_plans_utilization,
            TimePeriod=self._time_period(),
            Granularity="DAILY",
        ):
            for row in page.get("SavingsPlansUtilizationsByTime", []):
                util = row["Utilization"]
                records.append(UtilizationRecord(
                    commitment_id=row.get("Attributes", {}).get("SavingsPlanArn", "TOTAL"),
                    period_start=datetime.fromisoformat(row["TimePeriod"]["Start"]).date(),
                    period_end=datetime.fromisoformat(row["TimePeriod"]["End"]).date(),
                    utilization_pct=Decimal(util["UtilizationPercentage"]),
                    unused_commitment=Decimal(util["UnusedCommitment"]),
                    total_commitment=Decimal(util["TotalCommitment"]),
                ))
        logger.info("fetched %d Savings Plans utilization records", len(records))
        return records

    def fetch_reservation_utilization(self) -> List[UtilizationRecord]:
        records: List[UtilizationRecord] = []
        for page in self._paginate(
            self.ce.get_reservation_utilization,
            TimePeriod=self._time_period(),
            Granularity="DAILY",
        ):
            for row in page.get("UtilizationsByTime", []):
                util = row["Total"]
                records.append(UtilizationRecord(
                    commitment_id="RI_POOL",
                    period_start=datetime.fromisoformat(row["TimePeriod"]["Start"]).date(),
                    period_end=datetime.fromisoformat(row["TimePeriod"]["End"]).date(),
                    utilization_pct=Decimal(util["UtilizationPercentage"]),
                    unused_commitment=Decimal(util.get("UnusedHours", "0")),
                    total_commitment=Decimal(util.get("PurchasedHours", "0")),
                ))
        logger.info("fetched %d Reserved Instance utilization records", len(records))
        return records

    def fetch_savings_plans_coverage(self) -> List[CoverageRecord]:
        records: List[CoverageRecord] = []
        for page in self._paginate(
            self.ce.get_savings_plans_coverage,
            TimePeriod=self._time_period(),
            Granularity="DAILY",
        ):
            for row in page.get("SavingsPlansCoverages", []):
                cov = row["Coverage"]
                records.append(CoverageRecord(
                    dimension_key=row.get("Attributes", {}).get("INSTANCE_FAMILY", "ALL"),
                    period_start=datetime.fromisoformat(row["TimePeriod"]["Start"]).date(),
                    period_end=datetime.fromisoformat(row["TimePeriod"]["End"]).date(),
                    coverage_pct=Decimal(cov["CoveragePercentage"]),
                    on_demand_equivalent_cost=Decimal(cov["OnDemandCost"]),
                    covered_spend=Decimal(cov["SpendCoveredBySavingsPlans"]),
                ))
        logger.info("fetched %d Savings Plans coverage records", len(records))
        return records

    @staticmethod
    def effective_savings_rate(coverage: CoverageRecord, commitment_cost: Decimal) -> Decimal:
        """Blended discount actually realized against the on-demand-equivalent baseline."""
        baseline = coverage.on_demand_equivalent_cost + coverage.covered_spend
        if baseline <= 0:
            return Decimal("0")
        rate = (baseline - commitment_cost) / baseline
        return (rate * 100).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

    @staticmethod
    def break_even_days(
        upfront_cost: Decimal, daily_savings: Decimal, term_days: int
    ) -> Optional[int]:
        """Days until upfront cost is offset by realized savings; None if never."""
        if daily_savings <= 0:
            return None
        days = int((upfront_cost / daily_savings).to_integral_value(rounding=ROUND_HALF_UP))
        return days if days <= term_days else None

    def score(
        self,
        util: UtilizationRecord,
        cov: CoverageRecord,
        commitment_cost: Decimal,
        upfront_cost: Decimal,
        term_days: int = 365,
    ) -> CommitmentScore:
        """Composite score: rewards high utilization AND fast break-even, penalizes waste."""
        esr = self.effective_savings_rate(cov, commitment_cost)
        daily_savings = (cov.covered_spend - commitment_cost) / max(
            (cov.period_end - cov.period_start).days, 1
        )
        be_days = self.break_even_days(upfront_cost, daily_savings, term_days)

        flags: List[str] = []
        if util.utilization_pct < 70:
            flags.append("underutilized")
        if cov.coverage_pct < 50:
            flags.append("coverage_gap")
        if be_days is None:
            flags.append("no_break_even_in_term")

        # Score blends utilization and effective savings rate, penalized for flags;
        # bounded [0, 100] so recommendation ranking is stable across commitments.
        raw_score = (util.utilization_pct * Decimal("0.5")) + (esr * Decimal("0.5"))
        penalty = Decimal(len(flags) * 10)
        recommendation_score = max(Decimal("0"), min(Decimal("100"), raw_score - penalty))

        return CommitmentScore(
            commitment_id=util.commitment_id,
            utilization_pct=util.utilization_pct,
            coverage_pct=cov.coverage_pct,
            effective_savings_rate=esr,
            break_even_days=be_days,
            recommendation_score=recommendation_score.quantize(Decimal("0.01")),
            flags=flags,
        )

    def run(self) -> List[CommitmentScore]:
        logger.info("starting commitment discount scoring pass (%d-day lookback)", self.lookback_days)
        utils = self.fetch_savings_plans_utilization() + self.fetch_reservation_utilization()
        covs = self.fetch_savings_plans_coverage()
        scores: List[CommitmentScore] = []
        for util, cov in zip(utils, covs):
            commitment_cost = cov.covered_spend * (Decimal("1") - util.utilization_pct / 100 * Decimal("0.1"))
            upfront_cost = cov.covered_spend * Decimal("0.3")  # placeholder ratio; wire to real purchase records
            scores.append(self.score(util, cov, commitment_cost, upfront_cost))
        logger.info("scored %d commitments", len(scores))
        return scores


def main() -> None:
    engine = CommitmentDiscountEngine(lookback_days=30)
    scores = engine.run()
    for s in sorted(scores, key=lambda x: x.recommendation_score, reverse=True):
        logger.info(
            "commitment=%s util=%s%% coverage=%s%% esr=%s%% break_even=%s flags=%s score=%s",
            s.commitment_id, s.utilization_pct, s.coverage_pct,
            s.effective_savings_rate, s.break_even_days, s.flags, s.recommendation_score,
        )


if __name__ == "__main__":
    main()

The child page Calculating Savings Plan Coverage and Utilization extends this engine with the arithmetic detail behind effective_savings_rate and worked examples across partial-term and mid-term-purchase scenarios.

Schema Reference Table

The engine flattens three unrelated provider schemas onto one normalized commitment-scoring model. This mapping is what lets a single dashboard rank AWS Savings Plans against GCP CUDs against Azure Reservations on the same axis.

Provider field Normalized field Type Notes
AWS Utilization.UtilizationPercentage (Savings Plans / RI) utilization_pct decimal Denominator is purchased commitment, not eligible spend
AWS Coverage.CoveragePercentage coverage_pct decimal Denominator is on-demand-equivalent eligible spend
AWS Utilization.UnusedCommitment unused_commitment decimal Feeds the underutilized flag below 70% utilization
AWS SavingsPlansPurchaseRecommendationDetail.EstimatedSPCost commitment_cost decimal Amortized recurring commitment cost for the period
GCP credits[].amount where credits[].type = COMMITTED_USAGE_DISCOUNT cud_discount_amount decimal Negative in the export; sum and negate for realized savings
GCP Recommender content.overview.cudMetadata.recommendedCommitment recommended_commitment decimal Refreshed on a rolling ~24h cadence, not on-demand
Azure reservationSummaries.properties.avgUtilizationPercentage utilization_pct decimal Daily or monthly grain, per reservationId
Azure reservationDetails.properties.reservedHours / usedHours total_commitment / used_hours decimal Max 3-month window per call; chunk and stitch
Azure reservationRecommendations.properties.netSavings estimated_savings decimal Scope-dependent (single vs. shared) — verify before trusting
All providers, purchase-agnostic recommendation_score decimal (0–100) Derived; blends utilization and effective savings rate, penalized by flags

Operational Considerations

  • AWS Cost Explorer throttling. All Get*Utilization/Get*Coverage calls share the same account-level throttle, a default 5 requests per second, and Cost Explorer bills $0.01 per paginated API request regardless of whether it is a utilization, coverage, or recommendation call — serialize concurrent pulls per account rather than fanning out.
  • 13-month utilization/coverage history. AWS retains Savings Plans and RI utilization/coverage data for roughly 13 months; anything older must come from an already-persisted warehouse, not a fresh API call.
  • GCP Recommender refresh cadence. CommittedUseDiscountRecommender recommendations refresh on a rolling basis, typically every 24 hours; polling more frequently than that returns stale or identical results and burns quota against the Recommender API’s default 600 requests per minute per project.
  • Azure’s 3-month window cap. reservationDetails rejects any date range wider than 3 months in a single call; a 12-month lookback needs four sequential windowed calls, and the response has no server-side continuation token — track the next start date client-side.
  • Trailing-window volatility. All three providers restate the trailing few days of utilization/coverage as usage finalizes — treat the most recent 72 hours as provisional and re-pull it on every run rather than caching it as final.
  • Monitoring hooks. Emit utilization_pct, coverage_pct, effective_savings_rate, and recommendation_score per commitment to your metrics backend; alert when utilization drops below 70% for three consecutive daily pulls, or when break_even_days returns None for a commitment past 50% of its term — both signal a purchase that will not pay for itself.

Troubleshooting

GetSavingsPlansUtilization returns an empty result for a Savings Plan you know is active. Root cause: the call ran from a linked account rather than the management (payer) account, or the LINKED_ACCOUNT filter excluded the account that owns the commitment. Detection: the AWS Billing console shows active utilization but the API response is empty. Remediation: run the pull from the payer account with no LINKED_ACCOUNT filter, or explicitly include every linked account ID in the filter.

Utilization or coverage percentage exceeds 100%. Root cause: the same commitment was double-counted across a re-run that appended rather than replaced results, or a cross-account shared commitment was summed twice. Detection: any utilization_pct or coverage_pct above 100 in the scored output. Remediation: persist scoring runs with partition-scoped replace semantics keyed on (commitment_id, period_start, period_end), matching the idempotency pattern in Reserved Instance Mapping Logic.

ThrottlingException under concurrent multi-account pulls. Root cause: parallel workers exceed the shared 5 req/s Cost Explorer ceiling for the account. Detection: ClientError with code ThrottlingException in the _call wrapper’s logs. Remediation: the tenacity @retry with exponential backoff in the engine absorbs transient bursts; for sustained throughput, serialize calls per payer account and fan out across accounts instead of within one.

Azure reservationSummaries shows 0% utilization for a reservation you can see is active. Root cause: the query scope (Single vs. Shared) does not match how the reservation was purchased, so the summary is scoped to a subscription that never consumed it. Detection: the Azure portal shows nonzero utilization at the shared scope but the API call at subscription scope returns zero. Remediation: query at the reservation-order scope with grain=daily and no subscription filter first, then narrow once the correct scope is confirmed.

GCP CUD discount is missing from the credits array despite an active commitment. Root cause: the commitment only matches usage within its specific SKU family and region, and the usage row being checked falls outside that match — the discount silently does not apply rather than erroring. Detection: Recommender shows an active, fully-subscribed commitment, but a specific project’s billing rows carry no COMMITTED_USAGE_DISCOUNT credit. Remediation: cross-check the commitment’s region/type metadata against the SKU’s family before flagging it as a pipeline bug; this is expected behavior for out-of-scope usage, not a data-quality defect.

Frequently Asked Questions

What’s the difference between coverage and utilization for commitment discounts?

Utilization measures how much of the commitment you purchased is actually being consumed (used / purchased); coverage measures how much of your eligible spend is protected by a commitment (covered / (covered + on-demand)). A commitment can show 100% utilization while coverage stays low if you simply did not buy enough, or high coverage with low utilization if the commitment is oversized relative to actual usage. Both numbers must be scored together — the full mechanics of this reconciliation are covered in Reserved Instance Coverage vs Utilization Metrics.

How is effective savings rate calculated?

Effective savings rate is the blended discount actually realized against the on-demand-equivalent baseline: (baseline_cost − commitment_cost) / baseline_cost, where baseline_cost is the sum of covered spend and its on-demand equivalent from the coverage API response. It differs from the headline discount percentage a provider advertises because it accounts for unused commitment dragging the realized rate down — a Savings Plan advertised at a 30% discount but running at 60% utilization delivers a much lower effective rate.

Can Savings Plans and Reserved Instances be combined, and how do you avoid double counting?

Yes — AWS applies them in a strict, non-interchangeable order (compute Savings Plans, then EC2 Instance Savings Plans, then regional RIs, then zonal RIs), so a given usage-hour is only ever covered by one instrument. Double counting in the scoring pipeline happens at the data layer, not the billing layer: pulling GetSavingsPlansCoverage and GetReservationCoverage separately and summing their covered spend without checking for overlap in the underlying usage will overstate total coverage. The precedence rules that prevent this at the usage level are detailed in Reserved Instance Mapping Logic.

How far back can I pull Savings Plans utilization data via the Cost Explorer API?

Roughly 13 months. AWS retains detailed Savings Plans and Reserved Instance utilization and coverage history for about 13 months through the Cost Explorer API; anything older has to come from data your own pipeline already persisted, which is another reason to run the scoring engine on a recurring schedule rather than relying on ad hoc historical pulls.

How do GCP CUDs differ from AWS Savings Plans in terms of API-based measurement?

AWS exposes Savings Plans utilization and coverage as first-class, purpose-built Cost Explorer operations with percentage fields ready to consume. GCP has no equivalent utilization endpoint — realized CUD discounts have to be derived by summing the credits array in the BigQuery billing export filtered to COMMITTED_USAGE_DISCOUNT, and forward-looking recommendations come from a separate Recommender API on its own ~24-hour refresh cadence. This means the GCP branch of the pipeline does more normalization work to arrive at the same coverage/utilization shape AWS returns natively.

Up: FinOps Architecture & Billing Fundamentals