Skip to main content
Your progress
0 of 6 complete0%
T0 / M0.4 / L3 OF 5 / Operator LEVEL / 10 min

Amortized cost: Azure's gotcha

Outcome

By the end of this lesson, you will be able to explain why Azure ActualCost shows $0 for reservation-covered resources and use AmortizedCost as the correct column for per-resource reporting.


PrerequisitesL1, L2
Time10 minutes

1. Concept

Azure has one billing behaviour that quietly ruins cost reports, and almost everyone meets it within days of trying to report on committed spending.

It is documented and easy to miss. This lesson exists to make sure you do not miss it.

What Azure ActualCost shows for reservations

Azure’s Cost Management API exposes two cost columns: ActualCost and AmortizedCost.

Terminal window
ActualCost = the moment-of-charge cost, as billed to the subscription
AmortizedCost = the per-day, per-resource distributed cost

For pay-as-you-go resources, the two columns return the same number. For reservation-covered resources, they diverge:

Terminal window
RESERVATION PURCHASE (3-yr, $36,000 upfront)
ActualCost: $36,000 on day 1, $0 every other day for 3 years
AmortizedCost: $32.88 per day for 1,095 days
VM COVERED BY THAT RESERVATION (Standard_D8s_v3, 24/7)
ActualCost: $0 (the reservation is paid; the VM is free)
AmortizedCost: ~$32.88 per day attributed to this specific VM

The mechanic: Azure bills the reservation purchase against the subscription, not against each resource. The resource itself is “free” at the moment of consumption because the reservation has already been paid for. ActualCost faithfully reports this. But for any per-resource cost question, “what does this VM cost?”, ActualCost returns the misleading answer.

Why this matters

The naive reporting pipeline pulls ActualCost and groups by resource. The resulting report:

Terminal window
RESOURCE ACTUALCOST (May 2026)
─────────────────────────────────────────────────────────
production-vm-01 (D8s_v3, reserved) $ 0.00 ← WRONG
production-vm-02 (D8s_v3, reserved) $ 0.00 ← WRONG
analytics-vm-03 (D16s_v3, PAYG) $ 583.20
─────────────────────────────────────────────────────────
TOTAL VM COST $ 583.20 ← WRONG

The team concludes that reserved VMs are free. They are not. The reservation is being paid. The cost is just attributed at the subscription level, not the resource level, when ActualCost is the column.

The fix is to use AmortizedCost:

Terminal window
RESOURCE AMORTIZEDCOST (May 2026)
─────────────────────────────────────────────────────────
production-vm-01 (D8s_v3, reserved) $ 983.10 ← CORRECT
production-vm-02 (D8s_v3, reserved) $ 983.10 ← CORRECT
analytics-vm-03 (D16s_v3, PAYG) $ 583.20
─────────────────────────────────────────────────────────
TOTAL VM COST $2,549.40 ← CORRECT

Same resources. Same month. Same Azure account. The right column changes the number by a factor of four.

The five places this catches teams

  1. Per-resource cost reports. Any “cost per VM” or “cost per workload” report against subscription-scope data will show $0 for reserved resources if ActualCost is used.
  2. Showback / chargeback. Internal billing that uses ActualCost over-charges PAYG teams and under-charges reservation-backed teams.
  3. Recommendation engines. Cost optimization tools that read ActualCost will miss the savings opportunity on reserved resources (because they appear to cost nothing).
  4. Budget tracking. A team budget tracked against ActualCost will under-count reserved spend, leading to apparent surplus that is not real.
  5. Anomaly detection. Anomaly detection on ActualCost will not detect changes in reserved-resource usage.

Every one of these is a real, repeating customer story.

How AWS and GCP differ

AWS has the same trap, not an exemption. lineItem/UnblendedCost is the as-charged number, the AWS equivalent of Azure’s ActualCost: an all-upfront or partial-upfront RI posts its fee as a lump sum on the purchase date, and the hours it covers then show $0 unblended. That is exactly the per-resource $0 problem Azure has. This is why AWS ships a separate amortized view: Cost Explorer’s “Amortized costs” toggle, and the CUR’s lineItem/EffectiveCost (or the reservation//savingsPlan/ amortized columns), which spread the upfront fee across the covered hours. For per-resource reporting on AWS, use the amortized/EffectiveCost view, not UnblendedCost.

GCP works differently again. Its BigQuery billing export has no amortized field: the cost column is the charge before credits, and committed-use and sustained-use discounts arrive as separate negative amounts in the credits column. A CUD is billed as a flat monthly fee, so there is no upfront purchase to spread. To see what a CUD-covered VM really costs, add its credits to its cost.

So Azure is not the only cloud where an as-charged column can show $0 for committed capacity: AWS UnblendedCost does too for upfront RIs. Azure’s ActualCost is simply the case most teams meet first. In both clouds the as-charged column reflects the cash flow accurately; it just traps the per-resource reporting use case.

How ZopNight handles it

ZopNight pulls AmortizedCost from Azure Cost Management for all billing sync. The actual_cost_usd column in cost_records carries the amortized number, so per-resource reports always include the reservation distribution. This is non-negotiable for ZopNight’s Azure integration: the alternative (ActualCost) breaks too many downstream features.

The other two clouds are stored differently. AWS rows are stored as unblended cost, so on AWS the upfront-RI $0 trap above applies to ZopNight’s per-resource figures too. GCP rows are stored as cost plus credits (net of credits).

The per-row cost_source value is only ever calculated or actual. The report-level Cost Source label is what names the measure: on Azure-sourced reports it reads “Amortized Cost”.


2. Demo

The same Azure resource, queried two ways. ActualCost vs AmortizedCost:

Terminal window
# ActualCost: the trap
az costmanagement query \
--type ActualCost \
--timeframe MonthToDate \
--scope "/subscriptions/<sub-id>" \
--dataset-aggregation '{"totalCost":{"name":"Cost","function":"Sum"}}' \
--dataset-filter '{"dimensions":{"name":"ResourceId","operator":"In","values":["<vm-resource-id>"]}}'
# Returns: $0.00 (reservation-covered resource)
# AmortizedCost: the correct column
az costmanagement query \
--type AmortizedCost \
--timeframe MonthToDate \
--scope "/subscriptions/<sub-id>" \
--dataset-aggregation '{"totalCost":{"name":"Cost","function":"Sum"}}' \
--dataset-filter '{"dimensions":{"name":"ResourceId","operator":"In","values":["<vm-resource-id>"]}}'
# Returns: $983.10

Same resource. Same month. Two different numbers. AmortizedCost is the right one for per-resource reporting.


3. Hands-on (7 min)

If your organization runs Azure:

Terminal window
1. Identify one reservation-covered Azure VM.
2. Query its cost two ways from Cost Management (using Azure CLI or
the portal):
- Type=ActualCost, MonthToDate, filter by ResourceId
- Type=AmortizedCost, MonthToDate, filter by ResourceId
3. Compare the two numbers.
If you see $0 from ActualCost and a real number from AmortizedCost,
the gotcha is confirmed for your data.
4. Audit your team's existing Azure cost reports. Which column do they
use? If ActualCost, the report is silently undercounting reserved
resources.

4. Knowledge check

Q1

An Azure-focused engineer reports: “Our reserved D8s_v3 VMs cost $0 in Cost Management.” Most accurate response:

A. The reservation is broken
B. The query is using ActualCost
C. The VMs are not actually running
D. Cost Management has a bug

Show answer

Correct: B. Reservations show $0 there at subscription scope. Switch to AmortizedCost for per-resource reporting. This is the canonical Azure gotcha. The data is correct; the column choice is wrong for the question being asked.

Q2

A team builds a chargeback system that uses Azure ActualCost. Reserved-resource-heavy teams pay $0 in chargeback. PAYG-heavy teams pay the entire reservation purchase as a lump sum. Best fix:

A. Switch the chargeback system to AmortizedCost so the reservation cost is distributed across the covered resources
B. Disable reservations
C. Manually allocate the reservation purchase across the teams in a spreadsheet every month instead too
D. Pay teams equally

Show answer

Correct: A. AmortizedCost solves this exactly. The reservation purchase is distributed across consuming resources at the daily granularity needed for chargeback.

Q3

The Azure ActualCost / AmortizedCost trap exists because:

A. Azure billing is broken
B. Microsoft does not want users to know reservation cost at all
C. The trap is a Cost Management UI issue only
D. Reservations are paid at purchase time, not at consumption time

Show answer

Correct: D. ActualCost faithfully reports the cash flow; AmortizedCost distributes the cost for per-resource reporting. The two columns each correctly represent a different view of cost. The trap is choosing the wrong column for the question, not a bug in Azure.


5. Apply

ZopNight handles Azure cost sourcing automatically. The Cost Management integration pulls AmortizedCost by default. Reports always include reserved-resource cost in per-resource breakdowns.

To verify on your account:

  • Ask an AI assistant connected to ZopNight’s MCP server to run get_billing_sync_status. It shows whether billing sync is healthy for the Azure account: sync state, last successful sync and last error. Settings → Cloud Accounts → Edit → Cost source shows which feed capture chose for it.
  • The Cost Source label on the Reports header should read “Amortized Cost” for Azure-only data (or “Mixed (by provider)” when other clouds are included).
  • The reserved VM’s row in Cloud Estate → Inventory should show a non-zero cost.

If the reserved VM shows $0 or the label reads “Rack Rate” after billing sync has run, escalate: the sync is misconfigured.


Glossary terms touched

Amortized cost · Azure Reservation


Start with the bill.

Foundations takes about five hours. The first lesson is nine minutes.

Free curriculum. No paywall. One free account opens 314 lessons across 8 courses and three publicly verifiable credentials. Read it on the train, take the exam on a Saturday, list the credential on your résumé Monday.

5h median time to finish Foundations
free account, every course, no paywall
3 certifications, each with a public verifier
Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console· Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console·