AZ-900 cost management what moves the price, what a budget does not do, and why tags miss spend
Same VM, four regions, four prices. A budget that only sends email. A tagged resource group whose VMs show up untagged.
The problem
A small team runs two web virtual machines (VMs), a development VM, a database and a storage account in Azure. The first invoice comes in above their estimate, and nobody can say why, because the estimate assumed one region, one operating system and one way of paying. Then finance asks which cost center owns which part of the bill, and about a third comes back "untagged", although the resource groups were tagged months ago. Last, a manager admits to setting a budget equal to the estimate and assuming Azure would stop spending at the limit. It stopped nothing, and the alert arrived a day late.
These failures map to the Microsoft AZ-900 (Azure Fundamentals) exam, skills measured as of July 20, 2026. Under "Describe Azure management and governance (30–35%)", the group "Describe cost management in Azure" lists "Describe factors that can affect costs in Azure", "Explore the pricing calculator", "Describe cost management capabilities in Azure" and "Describe the purpose of tags". Under "Describe cloud concepts (25–30%)" sit "Describe the consumption-based model" and "Compare cloud pricing models". A score of 700 or greater is required to pass.
The idea, in plain words
Azure uses a consumption-based model: you pay for what you use instead of buying capacity up front, and the rate depends on choices you make, such as the resource type and its settings, the region, the licensing, whether you commit to a term, and the subscription type. The pricing calculator estimates those costs before anything is built, so that surprises appear on paper first. Microsoft Cost Management watches spending afterward and sends alerts, but only as fast as usage data arrives. Tags are name-value labels that let you split a bill by owner or cost center, and they work only where they are actually applied.
Tested with: the public Azure Retail Prices API (api-version=2023-01-01-preview), queried with curl and jq on 2026-10-19 at 14:05 ET (18:05 UTC). Budgets and tag inheritance come from Microsoft's documentation, and the tag example uses SYNTHETIC data.
The factors that change the price
A cost factor is any choice that changes the rate Azure charges for the same work. Microsoft Learn names six: resource type, consumption, maintenance, geography, subscription type and Azure Marketplace. Each one is an assumption an estimate can get wrong.
Resource type and settings set the base price. For a VM, the price reflects "licensing for the operating system or other software, the processor and number of cores for the VM, the attached storage, and the network interface." Consumption is how you pay. Pay-as-you-go bills for what runs. Reservations commit you to specific resources for one- or three-year terms, and Microsoft says they can cut costs by up to 72% from pay-as-you-go prices. A savings plan for compute commits you to an hourly spend for one or three years, and Azure applies the best available price as usage occurs. Azure Spot Virtual Machines use unused capacity at lower prices, but those workloads can be evicted when Azure needs the capacity back.
Maintenance covers what outlives a resource: deprovisioning a VM may not remove the storage and networking created with it, so those keep billing. Geography matters because power, labor, taxes and fees vary by location. Data transfer is priced by billing zone, a grouping of regions used only for that purpose and not the same thing as an availability zone; some inbound transfer is free. Some subscription types include usage allowances, and Marketplace purchases add the vendor's own billing.
Worked example: querying the Retail Prices API
The Azure Retail Prices API (application programming interface) at https://prices.azure.com/api/retail/prices gives, in Microsoft's words, "an unauthenticated experience to get retail rates for all Azure services", so this lab needs no account and provisions nothing. Prices are USD retail list prices fetched on 2026-10-19; they change, so the numbers are a snapshot and the method is the lesson. The script, prices.sh, calls the API with curl and picks rows with jq, a command-line JSON processor. Its filter is an OData (Open Data Protocol) expression, and armSkuName names the VM size. Here is the query function and section 1; sections 2 to 6 are in the lab files.
API='https://prices.azure.com/api/retail/prices?api-version=2023-01-01-preview'
SKU="${SKU:-Standard_D2s_v5}"
q() { # $1 = OData filter; prints the Items array
curl -sS -G "$API" --data-urlencode "\$filter=$1" | jq '.Items'
}
echo "== 1. Same VM size, Linux, pay-as-you-go, four regions (USD per hour)"
for r in eastus westeurope swedencentral brazilsouth; do
q "armRegionName eq '$r' and armSkuName eq '$SKU' and priceType eq 'Consumption'" |
jq -r --arg r "$r" '[.[] | select(.productName | endswith("Windows") | not)
| select(.meterName | test("Spot|Low Priority") | not)][0]
| "\($r)\t\(.retailPrice)\t\(.productName)\t\(.meterName)"'
done
The preview API version is there because savings plan rates are only returned with api-version=2023-01-01-preview. The function q sends a filter and returns the list of price rows. The loop asks for pay-as-you-go ("Consumption") rows in each region, then drops Windows rows and the discounted Spot and Low Priority rows before taking the first match. Without that filtering, Windows rows would add licensing and the discounted rows would pull prices down, and the regions would not be compared like for like.
fetched 2026-10-19T18:05:03Z
== 1. Same VM size, Linux, pay-as-you-go, four regions (USD per hour)
eastus 0.096 Virtual Machines Dsv5 Series D2s v5
westeurope 0.115 Virtual Machines Dsv5 Series D2s v5
swedencentral 0.102 Virtual Machines Dsv5 Series D2s v5
brazilsouth 0.153 Virtual Machines Dsv5 Series D2s v5
The size, operating system and payment method are identical in all four rows, yet the rate runs from 0.096 in eastus to 0.153 in brazilsouth. Only the region differs, so geography accounts for the gap, which is why the pricing calculator asks for a region. Section 2 fetches every eastus row for this size once and compares the ways to pay.
== 2. eastus, one VM size, the ways to pay (effective USD per hour)
pay-as-you-go, Linux 0.096
pay-as-you-go, Windows 0.188
Spot, Linux 0.020266
savings plan 1 yr 0.0658368
savings plan 3 yr 0.0441792
reservation 1 yr 519.0 per term = 0.0592 per hour
reservation 3 yr 997.0 per term = 0.0379 per hour
(reservation unitOfMeasure as returned: 1 Hour)
Linux at 0.096 against Windows at 0.188 is licensing: the same hardware costs more once the operating system license is included. Spot is the lowest rate. The savings plan rates sit inside the pay-as-you-go row, one per term. Reservation rows need care, because the API reports unitOfMeasure as "1 Hour" while the figure covers the whole term. Microsoft's own example row is priced 25007.0 with unit "1 Hour" and term "1 Year". The per-hour numbers are the lab's arithmetic: 519.0 divided by 8,760 hours, and 997.0 divided by 26,280.
== 3. Dev/Test price type for the same Windows VM (subscription type changes the bill)
Consumption 0.188
DevTestConsumption 0.096
== 4. Filter values are case-sensitive in this API version
serviceName eq 'Virtual Machines' -> 11 items
serviceName eq 'virtual machines' -> 0 items
The same Windows size costs 0.188 under Consumption and 0.096 under DevTestConsumption, so the subscription type changes the price. A filter value in the wrong case does not error; it returns nothing, because "in the 2023-01-01-preview version and later, the value is case sensitive." The last two sections check the filter that Trap 2 relies on and the version switch behind Trap 3:
== 5. priceType filter values
priceType eq Consumption: 6 rows, types Consumption
priceType eq DevTestConsumption: 3 rows, types DevTestConsumption
== 6. savingsPlan field with and without the preview api-version (Linux pay-as-you-go row)
/api/retail/prices: savingsPlan absent
/api/retail/prices?api-version=2023-01-01-preview: savingsPlan 2 terms
Filtering on priceType eq 'Consumption' returned six rows, all of type Consumption, so Dev/Test rows never reach the estimate. The same Linux row carried two savings plan terms with the preview version and no savingsPlan field without it. Pin the version in the URL, because a silent omission is harder to spot than an error.
Exam traps
Trap 1: Spot for steady work
- Symptom: a web tier priced at the Spot rate of 0.020266 disappears mid-day.
- Cause: Spot workloads can be evicted when Azure needs the capacity back.
- Fix: keep Spot for interruptible batch work and cover steady work with a reservation or savings plan. A reservation is a billing discount and does not change the runtime state of resources.
Trap 2: an estimate that looks too cheap
- Symptom: a price query returns rates well below the invoice.
- Cause: the query did not filter on type, so DevTestConsumption rows appeared next to Consumption rows.
- Fix: filter
priceType eq 'Consumption', as section 5 shows, and confirm the subscription type.
Trap 3: savings plan rates missing
- Symptom: the pay-as-you-go row has no
savingsPlanfield. - Cause: the request left out
api-version=2023-01-01-preview. - Fix: put the version in the URL and treat a missing field as a request bug, not a missing discount.
Estimate before, watch after
The pricing calculator estimates potential Azure expenses. Microsoft is explicit: "Nothing is provisioned when you add resources to the pricing calculator, and you won't be charged for any services you select." The Total Cost of Ownership (TCO) calculator has been retired, so study material that teaches it is out of date.
Cost Management is the tool for after the fact: cost analysis shows aggregated costs by subscription, resource group or service, and alerts and budgets watch spend. Alerts come in three types: budget alerts, credit alerts and department spending quota alerts. The last two are Enterprise Agreement (EA) features, and credit alerts fire automatically at 90% and 100% of the Azure credit balance.
A budget is a spending threshold on a scope, such as a subscription or resource group, with email alerts on actual or forecasted cost. The tutorial is blunt: "Resources aren't affected, and your consumption isn't stopped." Timing matters too: "Cost and usage data is typically available within 8-24 hours and budgets are evaluated against these costs every 24 hours," so an alert can trail real spend by a day or more. Budgets can trigger automation that suspends or modifies resources, but you have to build it.
Worked example: a budget for one VM
Take one Linux D2s v5 on pay-as-you-go in eastus at 0.096 USD per hour. Assuming an average month of 730 hours, the estimate is 0.096 × 730 = 70.08 USD. Microsoft's example alerts at 80% of a budget, here about 56.06 USD. Because data arrives 8 to 24 hours late and evaluation runs daily, the email may land a day after the line was crossed, and the VM keeps running unless automation acts. Microsoft describes forecast alerts as "advanced notification that your spending trends are likely to exceed your budget", so set one beside the actual-cost alert. The lab did not create a budget in a live subscription; this section comes from the documentation.
Trap: "the budget will stop the VMs"
- Symptom: spend passes the budget and the VMs are still running.
- Cause: a budget evaluates cost and notifies. It does not touch resources.
- Fix: alert early on forecasted cost, act on the email, or attach automation at a threshold.
Tags, and why they miss spend
A tag is a name-value pair of metadata on a resource, resource group or subscription. Microsoft suggests a starter set of Environment, Owner, CostCenter and Workload. You manage tags through the portal, PowerShell, the Azure CLI, Azure Resource Manager templates or the REST API, and Azure Policy can require tags or reapply removed ones.
The rule behind most surprises: "Resources don't inherit the tags you apply to a resource group or a subscription." Each resource, resource group and subscription can hold at most 50 tag name-value pairs, a name can run to 512 characters and a value to 256, names are case-insensitive for operations, and not every resource type supports tags.
Cost Management's tag inheritance setting fills the gap in reports. When enabled, it applies billing, resource group and subscription tags to child resource usage records, which are the billing lines and not the resources. The update takes 8 to 24 hours, and by default the resource's own tag wins when both exist.
Worked example: a rollup of synthetic usage
The file below is SYNTHETIC. It was made up for this lab and was not exported from any Azure account, and the inheritance setting was not exercised in a live subscription.
{
"_note": "SYNTHETIC usage records for a lab. Not exported from a real Azure account.",
"resourceGroups": {
"rg-shop-prod": { "CostCenter": "CC-410" },
"rg-shop-dev": { "CostCenter": "CC-410" },
"rg-data": {}
},
"usage": [
{ "resource": "vm-shop-web-1", "resourceGroup": "rg-shop-prod", "tags": { "CostCenter": "CC-410" }, "costUSD": 70.08 },
{ "resource": "vm-shop-web-2", "resourceGroup": "rg-shop-prod", "tags": {}, "costUSD": 70.08 },
{ "resource": "disk-shop-web-2", "resourceGroup": "rg-shop-prod", "tags": {}, "costUSD": 9.60 },
{ "resource": "vm-shop-dev", "resourceGroup": "rg-shop-dev", "tags": { "CostCenter": "CC-990" }, "costUSD": 35.04 },
{ "resource": "sql-orders", "resourceGroup": "rg-data", "tags": { "CostCenter": "CC-520" }, "costUSD": 120.00 },
{ "resource": "st-exports", "resourceGroup": "rg-data", "tags": {}, "costUSD": 14.25 }
]
}
Two resource groups carry CostCenter CC-410, and rg-data carries none. Only three of the six usage records have their own tags: vm-shop-web-1, vm-shop-dev and sql-orders. The second web VM, its disk and st-exports have empty tags. As with the team in the problem, the groups were tagged and the resources were not, which gives the script one record for each case it must handle.
#!/usr/bin/env bash
# Group SYNTHETIC usage cost by the CostCenter tag, two ways:
# A. resource tags only (resources do not inherit resource-group tags)
# B. as Cost Management's tag inheritance setting applies them to usage
# records: the resource-group tag fills a gap, the resource tag wins
# when both exist (the default).
set -euo pipefail
F="${1:-usage-SYNTHETIC.json}"
echo "== A. resource tags only"
jq -r '.usage | group_by(.tags.CostCenter // "untagged")
| map({cc: (.[0].tags.CostCenter // "untagged"), usd: (map(.costUSD) | add * 100 | round / 100)})
| .[] | "\(.cc)\t\(.usd)"' "$F"
echo "== B. with tag inheritance on usage records (resource tag wins)"
jq -r '.resourceGroups as $rg | .usage
| map(. + {cc: (.tags.CostCenter // $rg[.resourceGroup].CostCenter // "untagged")})
| group_by(.cc) | map({cc: .[0].cc, usd: (map(.costUSD) | add * 100 | round / 100)})
| .[] | "\(.cc)\t\(.usd)"' "$F"
Rollup A groups cost by each record's own CostCenter tag and falls back to "untagged". Rollup B uses a fallback chain: the resource's tag first, then its resource group's tag, then "untagged". The // operator in jq means "use the left value unless it is missing", which is how the resource tag wins. Both round each sum to cents. The script imitates the documented rule; it is not Azure's implementation, which runs inside Cost Management.
== A. resource tags only
CC-410 70.08
CC-520 120
CC-990 35.04
untagged 93.93
== B. with tag inheritance on usage records (resource tag wins)
CC-410 149.76
CC-520 120
CC-990 35.04
untagged 14.25
In A, 93.93 USD is untagged: the second web VM (70.08), its disk (9.60) and the storage account (14.25). In B, vm-shop-web-2 and its disk pick up CC-410 from rg-shop-prod, so CC-410 rises to 149.76. vm-shop-dev keeps its own CC-990 although its group says CC-410, because the resource tag wins. st-exports stays untagged because rg-data has no CostCenter tag, which is how a report still shows ownerless spend after inheritance is on.
Trap: "I tagged the resource group, so the VM is tagged"
- Symptom: the VM's own tag list is empty, and cost reports show it as untagged.
- Cause: resources do not inherit group or subscription tags. Only usage records receive them, and only with inheritance on.
- Fix: tag resources at creation and enforce it with Azure Policy. Use tag inheritance as a reporting safety net, and expect 8 to 24 hours of delay.
Putting it together
For the team in the problem: estimate in the pricing calculator with the real region, operating system and commitment; tag every resource at creation and enforce it with Azure Policy; turn on tag inheritance for reporting; and set actual and forecast budget alerts, deciding deliberately whether automation should act, because the budget alone never will.
Check yourself
api-version parameter. Savings plan rates come back only with 2023-01-01-preview.Sources
- Microsoft Learn, Study guide for Exam AZ-900 (skills measured as of July 20, 2026): https://learn.microsoft.com/en-us/credentials/certifications/resources/study-guides/az-900
- Microsoft Learn, Describe factors that can affect costs in Azure: https://learn.microsoft.com/en-us/training/modules/describe-cost-management-azure/2-describe-factors-affect-costs-azure
- Microsoft Learn, Explore the pricing calculator: https://learn.microsoft.com/en-us/training/modules/describe-cost-management-azure/3-compare-pricing-total-cost-of-ownership-calculators
- Microsoft Learn, Azure Retail Prices REST API overview: https://learn.microsoft.com/en-us/rest/api/cost-management/retail-prices/azure-retail-prices
- Microsoft Learn, Describe the Microsoft Cost Management tool: https://learn.microsoft.com/en-us/training/modules/describe-cost-management-azure/6-describe-azure-tool
- Microsoft Learn, What are Azure Reservations?: https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/save-compute-costs-reservations
- Microsoft Learn, Tutorial: Create and manage budgets: https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/tutorial-acm-create-budgets
- Microsoft Learn, Describe the purpose of tags: https://learn.microsoft.com/en-us/training/modules/describe-cost-management-azure/7-describe-purpose-of-tags
- Microsoft Learn, Use tags to organize your Azure resources: https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources
- Microsoft Learn, Group and allocate costs using tag inheritance: https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/enable-tag-inheritance
- Azure fundamentals study notes (vicjor/az-900): https://github.com/vicjor/az-900