Multi-cloudBill analysis6 min read

Cloud bill up? Check credits and coverage before cuts

Explain a monthly increase with reconciled usage, rate, coverage and credit changes, using AWS CUR 2.0 and Google billing export queries.

Prices and sources checked September 26, 2026
On this page
IN SHORT

A monthly increase can come mostly from a lost credit, weaker commitment coverage and tax, with little extra usage. Split every dollar of the change on one cost basis before asking the largest workload to absorb a cut.

Compute is the largest line on this illustrative bill, and it explains $2,140 of a $93,600 increase, so cutting it would leave $91,460 of the rise in place. Compute went from $214,000 to $216,140 while the whole bill rose 18%, and finance wants the increase explained before a leadership meeting. July and August are compared on the same account scope, in USD, on a net amortized basis, with tax shown separately.

The largest service explains only 2.3% of the $93,600 increase

The two months total $520,000 and $613,600. The other $91,460 came from database usage, lost commitment coverage, unused reservation fees, an ended promotional credit and tax.

ComponentJulyAugustChange
Compute$214,000$216,140+$2,140
Database usage, at July's effective rate$100,000$112,800+$12,800
Additional database cost from lost coverage$0$38,400+$38,400
Unused reservation fees$0$9,100+$9,100
Promotional credit-$25,000$0+$25,000
Tax$20,000$26,160+$6,160
Other services, combined$211,000$211,000$0
Total$520,000$613,600+$93,600

Every row is an illustrative total, so no meter, region or rate sits behind it. On real billing exports, the AWS query below separates volume, rate, coverage, credit and tax effects and leaves a remainder to reconcile, and the Google query separates cost before credits from the credits themselves.

Extra consumption contributes $2,140 + $12,800 = $14,940, or 16% of the increase, and that includes database growth for a planned environment. The remaining $78,660 comes from commitment effects, an exhausted credit and tax.

In the example, a database migration moved usage onto resources the existing Reserved Instances (RIs) no longer covered. That single change produced two charges: $38,400 more for uncovered usage and $9,100 of unused reservation fees, each counted once. The workload owner and the commitment owner need to review the migration together, because the two charges share one cause.

For finance, the note that matters names owners: the database and commitment owners explain the coverage gap together, and the workload owner confirms whether the $12,800 of database growth matches the planned environment.

Amortized cost explains consumption; actual cost explains purchases

Use AWS amortized cost for workload trends once Savings Plans or Reservations exist, and choose net amortized to include applicable discounts. Amortization spreads upfront and recurring reservation fees across the period, which avoids the charge-day spike that unblended cost shows.

Azure splits the same decision in two: Amortized Cost explains reservation consumption, and Actual Cost reconciles to the bill. Reservation-covered usage carries zero EffectivePrice in actual data and a prorated reservation price in amortized data.

Purchases come from actual data with ChargeType = Purchase. Unused reservations come from amortized data with ChargeType = UnusedReservation, and unused savings plans with ChargeType = UnusedSavingsPlan and PricingModel = SavingsPlan. Amortized data holds no purchase records, so adding purchases to it counts the commitment twice.

For Google Cloud, compare cost plus credits and keep both components visible, so an expiring credit does not vanish into a net figure. The cost field already includes negotiated discounts under the applicable consumption model, and calling it list-price spending would misstate the baseline.

Price the quantity change before attributing a coverage change

For a usage type present in both months, multiply the quantity change by the prior month's effective rate, then price the rate change at the current quantity. That allocates the interaction once. Usage types without comparable quantities belong in a separate new-or-removed category.

Start with the driver that changed, and involve the workload owner when quantity, region, SKU or coverage moved. A larger unused commitment balance is a workload question before it is a purchasing question. Reducing volume comes last, after its owner confirms the demand is unnecessary.

Azure cost details carry Quantity, UnitPrice, EffectivePrice and PricingModel for that work. A virtual-machine resize can surface as a new meter replacing a removed one, so offset the retired meter's drop against the new meter's cost before attributing growth.

CUR 2.0 separates diagnostic effects without proving their cause

Athena queries AWS Cost and Usage Report (CUR) 2.0 exports with SQL. Have an authorized billing colleague run this read-only query against your CUR 2.0 table, over the same scope and all services in both months. The dates are example inputs, and Athena engine v3 is required.

SQL
WITH params AS (
  SELECT TIMESTAMP '2026-07-01 00:00:00' AS base_month,
         TIMESTAMP '2026-08-01 00:00:00' AS comp_month
),
src AS (
  SELECT
    c.bill_billing_period_start_date AS billing_month,
    c.line_item_product_code AS product_code,
    c.line_item_usage_type AS usage_type,
    c.line_item_line_item_type AS li_type,
    COALESCE(c.line_item_usage_amount, 0) AS usage_amount,
    COALESCE(c.line_item_unblended_cost, 0) AS unblended_cost,
    COALESCE(c.pricing_public_on_demand_cost, 0) AS public_od_cost,
    CASE c.line_item_line_item_type
      WHEN 'SavingsPlanCoveredUsage' THEN COALESCE(c.savings_plan_savings_plan_effective_cost, 0)
      WHEN 'DiscountedUsage' THEN COALESCE(c.reservation_effective_cost, 0)
      WHEN 'Usage' THEN COALESCE(c.line_item_unblended_cost, 0)
      ELSE 0
    END AS usage_effective_cost,
    CASE c.line_item_line_item_type
      WHEN 'SavingsPlanRecurringFee' THEN COALESCE(c.savings_plan_total_commitment_to_date, 0)
                                        - COALESCE(c.savings_plan_used_commitment, 0)
      WHEN 'RIFee' THEN COALESCE(c.reservation_unused_amortized_upfront_fee_for_billing_period, 0)
                      + COALESCE(c.reservation_unused_recurring_fee, 0)
      ELSE 0
    END AS unused_commitment_cost
  FROM cur2 c CROSS JOIN params p
  WHERE c.bill_billing_period_start_date IN (p.base_month, p.comp_month)
),
usage_grain AS (
  SELECT s.product_code, s.usage_type,
    SUM(CASE WHEN s.billing_month = p.base_month THEN s.usage_amount END) AS q1,
    SUM(CASE WHEN s.billing_month = p.comp_month THEN s.usage_amount END) AS q2,
    SUM(CASE WHEN s.billing_month = p.base_month THEN s.usage_effective_cost END) AS c1,
    SUM(CASE WHEN s.billing_month = p.comp_month THEN s.usage_effective_cost END) AS c2,
    SUM(CASE WHEN s.billing_month = p.base_month THEN s.public_od_cost END) AS l1,
    SUM(CASE WHEN s.billing_month = p.comp_month THEN s.public_od_cost END) AS l2
  FROM src s CROSS JOIN params p
  WHERE s.li_type IN ('Usage', 'DiscountedUsage', 'SavingsPlanCoveredUsage')
  GROUP BY 1, 2
),
usage_effects AS (
  SELECT product_code,
    CASE WHEN COALESCE(q1,0) > 0 AND COALESCE(q2,0) > 0
         THEN (q2 - q1) * (c1 / q1) ELSE 0 END AS volume_effect,
    CASE WHEN COALESCE(q1,0) > 0 AND COALESCE(q2,0) > 0
         THEN ((l2 / q2) - (l1 / q1)) * q2 ELSE 0 END AS list_price_effect,
    CASE WHEN COALESCE(q1,0) > 0 AND COALESCE(q2,0) > 0
         THEN (((c2 / q2) - (c1 / q1)) - ((l2 / q2) - (l1 / q1))) * q2
         ELSE 0 END AS discount_coverage_effect,
    CASE WHEN COALESCE(q1,0) > 0 AND COALESCE(q2,0) > 0
         THEN 0 ELSE COALESCE(c2,0) - COALESCE(c1,0) END AS new_removed_effect
  FROM usage_grain
),
usage_by_service AS (
  SELECT product_code,
    SUM(volume_effect) AS volume_effect,
    SUM(list_price_effect) AS list_price_effect,
    SUM(discount_coverage_effect) AS discount_coverage_effect,
    SUM(new_removed_effect) AS new_removed_effect
  FROM usage_effects GROUP BY 1
),
other_by_service AS (
  SELECT s.product_code,
    SUM(CASE WHEN s.billing_month = p.comp_month THEN s.unblended_cost ELSE -s.unblended_cost END)
      FILTER (WHERE s.li_type IN ('Credit', 'Refund', 'Discount', 'BundledDiscount'))
      AS credits_refunds_discounts_effect,
    SUM(CASE WHEN s.billing_month = p.comp_month
             THEN s.unused_commitment_cost ELSE -s.unused_commitment_cost END)
      AS unused_commitment_effect,
    SUM(CASE WHEN s.billing_month = p.comp_month THEN s.unblended_cost ELSE -s.unblended_cost END)
      FILTER (WHERE s.li_type = 'Tax') AS tax_effect
  FROM src s CROSS JOIN params p
  GROUP BY 1
),
effects AS (
  SELECT o.product_code,
    COALESCE(u.volume_effect, 0) AS volume_effect,
    COALESCE(u.list_price_effect, 0) AS list_price_effect,
    COALESCE(u.discount_coverage_effect, 0) AS discount_coverage_effect,
    COALESCE(u.new_removed_effect, 0) AS new_removed_effect,
    COALESCE(o.credits_refunds_discounts_effect, 0) AS credits_refunds_discounts_effect,
    COALESCE(o.unused_commitment_effect, 0) AS unused_commitment_effect,
    COALESCE(o.tax_effect, 0) AS tax_effect
  FROM other_by_service o
  LEFT JOIN usage_by_service u ON u.product_code IS NOT DISTINCT FROM o.product_code
)
SELECT *,
  volume_effect + list_price_effect + discount_coverage_effect
    + new_removed_effect + credits_refunds_discounts_effect
    + unused_commitment_effect + tax_effect AS total_change
FROM effects
ORDER BY ABS(total_change) DESC;

Positive columns increase the modeled cost and negative columns offset it. total_change sums the diagnostic effects only, excluding net-discount fields and unmodeled subscription charges, so it cannot reproduce a billed or complete net-amortized total. Compare the summed effects with the change between the two months' totals on the selected cost metric, and report the difference as unexplained, then work through the remaining charge types, starting with FlateRateSubscription. Upfront fees and SavingsPlanNegation stay out because the effective-cost fields already allocate commitment costs.

discount_coverage_effect is the effective-rate residual left after the public-rate change. Coverage and usage mix both sit in it, so check both before blaming the migration. On tiered services such as S3 and data transfer, list_price_effect is approximate, because the public on-demand cost uses the highest tier. For size-flexible RIs, AWS's dictionary says to use reservation/TotalReservedUnits instead of line_item_usage_amount, so this query's volume effect on those rows is approximate.

Google credits must be added without duplicating usage cost

Use BigQuery against Google's standard billing export, with an authorized colleague substituting the table path and the example invoice months. The query covers every cost_type, including tax, adjustments and rounding errors.

SQL
SELECT
  service.description AS service,
  SUM(IF(invoice.month = '202607', cost, 0)) AS cost_base,
  SUM(IF(invoice.month = '202608', cost, 0)) AS cost_comp,
  SUM(IF(invoice.month = '202607', IFNULL((SELECT SUM(c.amount) FROM UNNEST(credits) c), 0), 0)) AS credits_base,
  SUM(IF(invoice.month = '202608', IFNULL((SELECT SUM(c.amount) FROM UNNEST(credits) c), 0), 0)) AS credits_comp,
  SUM(IF(invoice.month = '202608', cost, 0))
    - SUM(IF(invoice.month = '202607', cost, 0)) AS gross_cost_change,
  SUM(IF(invoice.month = '202608', IFNULL((SELECT SUM(c.amount) FROM UNNEST(credits) c), 0), 0))
    - SUM(IF(invoice.month = '202607', IFNULL((SELECT SUM(c.amount) FROM UNNEST(credits) c), 0), 0)) AS credits_change,
  SUM(IF(invoice.month = '202608', cost + IFNULL((SELECT SUM(c.amount) FROM UNNEST(credits) c), 0), 0))
    - SUM(IF(invoice.month = '202607', cost + IFNULL((SELECT SUM(c.amount) FROM UNNEST(credits) c), 0), 0)) AS net_change
FROM `project-ID.dataset.gcp_billing_export_v1_XXXXXX_XXXXXX_XXXXXX`
WHERE invoice.month IN ('202607', '202608')
GROUP BY service
ORDER BY ABS(net_change) DESC;

The correlated credit sum counts each cost row once and returns zero where credits are absent. gross_cost_change is before credits and net_change adds them, so a positive credits_change means credits became less negative.

net_change separates cost from credits but says nothing about quantity versus price, so price the quantity change first with usage.amount and cost_at_list from the same export. Filtering to cost_type = 'regular' also drops invoice components that still need their own reconciliation.

Group on invoice.month so late-reported usage stays with the invoice that carried it, then request a breakdown by credits.type for the leading services. A shrinking PROMOTION or FREE_TIER credit needs checks for expiry, exhaustion, changed eligible usage or adjustments. SUSTAINED_USAGE_DISCOUNT needs eligibility and runtime checks, and amounts alone establish no cause.

On AWS, credits apply until they are exhausted or they expire, and the estimated remaining balance updates daily. Ask billing which of the two ended the credit.

Check month length, invoice status and currency before assigning blame

Equal hourly demand across 31 days costs 10.7% more than in a 28-day month (31 ÷ 28 − 1 = 10.7%). Compare daily rates next to complete-month totals, and keep estimated and forecast figures visibly apart from invoiced amounts.

A blank AWS bill_invoice_id means the report is not final, and a populated one does not rule out later adjustments. CUR 2.0 can also update the previous billing period during the first 2 weeks after month-end.

Azure Enterprise Agreement and Microsoft Customer Agreement data typically arrives within 8 to 24 hours, and the period typically closes within 72 hours. Open-period estimates omit credits and price at the highest tier, which can overstate tiered products.

Keep both months in one documented currency and record exchange-rate effects separately. AWS bills in US dollars by default and converts to a preferred payment currency, so a flat USD bill can still rise locally. Azure exposes ExchangeRatePricingToBilling, and Google's cost ÷ currency_conversion_rate gives the USD cost. Use those documented conversions where needed, without assuming every multi-cloud comparison belongs in USD.

Native tools rank leads; the remainder still needs reconciliation

AWS Cost Comparison ranks absolute changes and returns the top 10 per dimension, leaving the remainder to reconcile separately. The driver labels in the AWS example are USAGE_CHANGE, RESERVATION_APPLIED_USAGE_CHANGE and CREDIT_USAGE_CHANGE. Use those labels to decide what to investigate first, and reconcile their amounts separately from the query's effects. That example is not a complete label list. The permissions are ce:GetCostAndUsageComparisons and ce:GetCostComparisonDrivers; a colleague holding them can pull the comparison on the selected metric.

The API takes exactly 1 month per interval, starting and ending on the first day of a month. History reaches 13 months, or 38 with multi-year monthly data. Its driver labels point the investigation somewhere without confirming the operational cause.

Azure anomaly insights sit in Cost Analysis smart views at subscription scope and run 36 hours after the UTC day ends, comparing the day against a forecast built on the preceding 60 days. So a morning without an alert says nothing yet about the previous day's spending. Google's cost breakdown report shows invoice-level charges only in Invoice month view, where underused spend-based commitments can appear as a charge bar.

When this does not apply

Skip this investigation if a documented reconciliation already explains the entire change, including usage, rates, commitments, credits, billing periods, tax and currency.

Sources

CloudFinOpsPro guides are researched, written, independently reviewed and edited by an automated editorial pipeline, then checked against the official sources listed above. How we work.