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.
| Tier | Operator |
| JTBD | ”Stop reporting that our reserved Azure VMs cost zero.” |
| Personas | FinOps Analyst · Finance Partner · Engineering Leader |
| Prerequisites | L1, L2 |
| Time | 10 minutes |
| Bloom verb | Explain (Understand) and Use (Apply) |
1. Concept
Azure has a billing-data behaviour that breaks naive cost reports. Engineers and FinOps Analysts run into it within days of trying to report reserved-instance costs at the resource level. The behaviour is documented but easy to miss. This lesson is the explicit warning.
What Azure ActualCost shows for reservations
Azure’s Cost Management API exposes two cost columns: ActualCost and AmortizedCost.
ActualCost = the moment-of-charge cost, as billed to the subscriptionAmortizedCost = the per-day, per-resource distributed costFor pay-as-you-go resources, the two columns return the same number. For reservation-covered resources, they diverge:
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 VMThe 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:
RESOURCE ACTUALCOST (May 2026)─────────────────────────────────────────────────────────production-vm-01 (D8s_v3, reserved) $ 0.00 ← WRONGproduction-vm-02 (D8s_v3, reserved) $ 0.00 ← WRONGanalytics-vm-03 (D16s_v3, PAYG) $ 583.20─────────────────────────────────────────────────────────TOTAL VM COST $ 583.20 ← WRONGThe 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:
RESOURCE AMORTIZEDCOST (May 2026)─────────────────────────────────────────────────────────production-vm-01 (D8s_v3, reserved) $ 983.10 ← CORRECTproduction-vm-02 (D8s_v3, reserved) $ 983.10 ← CORRECTanalytics-vm-03 (D16s_v3, PAYG) $ 583.20─────────────────────────────────────────────────────────TOTAL VM COST $2,549.40 ← CORRECTSame resources. Same month. Same Azure account. The right column changes the number by a factor of four.
The five places this catches teams
- 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.
- Showback / chargeback. Internal billing that uses ActualCost over-charges PAYG teams and under-charges reservation-backed teams.
- Recommendation engines. Cost optimization tools that read ActualCost will miss the savings opportunity on reserved resources (because they appear to cost nothing).
- Budget tracking. A team budget tracked against ActualCost will under-count reserved spend, leading to apparent surplus that is not real.
- 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 also avoids the trap. CUD discounts are applied at the resource level on the BigQuery billing export. The cost column for a CUD-covered Compute Engine VM shows the post-discount amortized cost.
Azure is the only major cloud where the default cost column (depending on how you query) can return $0 for reservation-covered resources. This is a deliberate design choice (ActualCost reflects the cash flow accurately), but it 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 cost_source label on Azure-sourced records is amortized to make this explicit in the data. Reports surface a tooltip on Azure rows clarifying the column choice.
2. Demo
The same Azure resource, queried two ways. ActualCost vs AmortizedCost:
# ActualCost: the trapaz 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 columnaz 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.10Same 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:
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 ResourceId3. 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. Reservations show $0 there at subscription scope. Switch to AmortizedCost for per-resource reporting.
C. Cost Management has a bug
D. The VMs are not actually running
Show answer
Correct: B. 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. Disable reservations
B. Switch the chargeback system to AmortizedCost so the reservation cost is distributed across the covered resources
C. Manually allocate the reservation purchase
D. Pay teams equally
Show answer
Correct: B. 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. Reservations are paid at purchase time, not at consumption time. ActualCost faithfully reports the cash flow; AmortizedCost distributes the cost for per-resource reporting.
C. Microsoft does not want users to know reservation cost
D. The trap is a Cost Management UI issue only
Show answer
Correct: B. 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:
- Cloud Accounts → click Azure account → View Sync Status shows the cost-source label. For Azure, this should read “Amortized”: confirming the right column is being pulled.
- Reports → Cost Breakdown filtered to one reserved VM should return a non-zero cost.
If either shows $0 or labels as ActualCost, escalate: the sync is misconfigured.
Related lessons
- L4: The two-source cost model (next)
- L2: Billing cost and the discount stack
- T2.M2.1: The 450+ rule library
Glossary terms touched
Amortized cost · ActualCost · Azure reservation · Per-resource attribution