How to Audit Your Azure Costs with Native Tools (Cost Management, Advisor, Azure Monitor)
Azure never bills you incorrectly. It bills you precisely, to four decimal places, for the unattached Premium SSD nobody remembers creating, the staging VM that last served a request in March, and the Application Gateway in front of a service you decommissioned last quarter. An Azure cost audit is how you find those line items before they compound, and you can run one end to end with tooling Microsoft already ships: Cost Management + Billing, Azure Advisor, Azure Monitor and the az CLI. No third-party platform, no agent installs, no procurement conversation.
Part one named the seven places Azure bills leak: unattached managed disks and stale snapshots, idle or oversized VMs, reservation and savings plan coverage gaps, unused public IPs and idle load balancers or Application Gateways, over-provisioned Azure SQL, bandwidth charges, and non-production environments running 24/7. This Azure cost management tutorial works through all seven, grouped by tool rather than by leak, because that's how the work actually flows: you open one blade or one terminal and take everything it can tell you before moving on.
01Prioritize
Cost Management
Read the bill by service, resource group, tag and pricing model before touching anything.
02Shortlist
Azure Advisor
Free, pre-computed recommendations for VMs, reservations and idle gateways.
03Prove
Azure Monitor
Thirty days of metrics that confirm a resource is idle before anyone resizes it.
04Sweep
az CLI
Inventory queries for the leaks Advisor never reports: disks, snapshots, IPs, stopped VMs.
Plan for three to four focused hours on a first pass across a handful of subscriptions. One prerequisite: you need at least the Cost Management Reader role on every subscription in scope, plus Reader for the CLI inventory queries. If a subscription is missing from your view, the audit silently under-reports, so confirm scope first with az account list --output table.
Cost Management + Billing: read the bill before touching a resource
Everything in this section lives at Cost Management + Billing → Cost Management → Cost analysis (Microsoft Learn: quickstart). Resist the urge to start deleting disks. Ten minutes here decides which of the seven leaks deserves your afternoon and which is a rounding error in your tenant.
Group by service, resource group and tag
Open Cost analysis, set the range to the last three months with Monthly granularity, and work through three groupings in order:
- Group by: Service name. This is your priority list. If Virtual Machines, Storage and Azure SQL carry most of the bill (the usual pattern at a mid-market SaaS), VM rightsizing, disk hygiene and SQL tiers are where the hours below pay off. If Bandwidth or a data service dominates, reweight.
- Group by: Resource group. Sort descending and scan for names you can't explain: old project codenames, a departed engineer's initials, anything ending in
-testor-pocthat's somehow still accruing. Each unexplained resource group is a finding. Write it down now and chase ownership later. - Group by: Tag, keyed on
environment(or your equivalent). This measures leak seven directly. A weekday business schedule is roughly 60 of the week's 168 hours, so any non-production compute cost above about 36% of its always-on equivalent is money spent while nobody is working. Check the untagged slice too. If it's a double-digit percentage of spend, poor tagging is itself a finding, because it caps how precise every future audit can be.
For bandwidth (leak six), stay in Cost analysis, filter Service name = Bandwidth, then switch Group by to Meter. The meter names separate internet egress from inter-region transfer. There's no universal healthy number here, so audit the trend: bandwidth growing faster than request volume usually means chatty cross-region replication or traffic taking a paid public path that a private one would serve. Flag it for an architecture conversation rather than a quick fix.
Budgets and alerts: make the next audit interrupt-driven
Before leaving the blade, go to Cost Management → Budgets and create one budget per subscription (Microsoft Learn: create budgets). Set the amount to last month's actuals plus expected growth, with alerts at 80% and 100% of Actual cost and one at 100% of Forecasted cost. Budgets fix nothing. They're tripwires. But the forecasted alert turns "we found the overrun at invoice time" into an email mid-month, while the spend is still stoppable.
Reservation and savings plan coverage
Leak three hides in plain sight in Cost analysis. Change Group by to Pricing model and switch the metric to Amortized cost, so reservation purchases spread across their term instead of distorting the purchase month. The chart now splits spend into On Demand, Reservation, Savings Plan and Spot.
The number to interrogate is the on-demand share of your stable baseline: compute that runs around the clock regardless of load. A healthy target is roughly 70 to 85% commitment coverage of that baseline, with the rest on demand for elasticity and churn. Well below that, you're paying list price for capacity whose usage you could predict, and Microsoft's published reservation discounts reach 72% for three-year terms (Microsoft Learn: reservations), so the gap is rarely small money. Don't chase 100%. Every point of coverage above your true baseline becomes a commitment you have to feed.
Coverage is half the ledger. Open Cost Management + Billing → Reservations and check each reservation's utilization (Microsoft Learn: reservation utilization) and its expiry date. Utilization persistently below the mid-90s means an earlier commitment outlived its workload. Instance size flexibility within the VM family may absorb it, and exchanging the reservation beats buying more coverage on top of a leak. Read coverage and utilization together before any purchase. Buying coverage while existing reservations sit half-used just moves the waste.
Azure Advisor: recommendations you already have, read correctly
Advisor's Cost category is the closest thing to a free pre-run audit: generated continuously, zero setup, already waiting at Advisor → Cost (Microsoft Learn: Advisor cost recommendations). The CLI equivalent:
az advisor recommendation list --category Cost --output table
Here's how Advisor recommendations map onto the leaks from part one:
| Advisor recommendation | Leak it covers |
|---|---|
| Right-size or shutdown underutilized virtual machines | Idle and oversized VMs |
| Buy reserved virtual machine instances | Reservation coverage gaps |
| Buy an Azure savings plan for compute | Savings plan coverage gaps |
| Delete or reconfigure idle virtual network gateways | Unused network infrastructure |
| Delete unprovisioned ExpressRoute circuits | Unused network infrastructure |
Read two properties of this list before you treat it as your findings document.
The savings estimates are directional. Each recommendation carries an estimated annual saving computed from retail rates and recent usage. It doesn't know your negotiated discounts, your amortization, or the batch job that runs on the 1st of the month. Use the figures to rank findings (the ordering holds up even when the dollars don't), and never paste them into a business case as committed savings.
The VM check is conservative by default. The default low-CPU threshold is 5%, so a VM idling at 12% is still mostly wasted and Advisor stays quiet about it. Widen the net:
az advisor configuration update --low-cpu-threshold 20
Recommendations regenerate against the new threshold within about a day. Even then, treat every flagged VM as a candidate. A short lookback can't see a monthly close or a quarterly report job, which is what the Azure Monitor pass below is for.
Look at what the table doesn't contain: unattached disks, aging snapshots, orphaned public IPs, over-provisioned Azure SQL, bandwidth and non-prod scheduling are absent or only partly covered. Roughly half of part one's leaks never show up in Advisor, which is why the CLI section exists.
For the findings document, use Download as CSV on the Advisor page (or redirect the CLI output to a file) and put the export date in the filename. You'll care about staleness later.
Azure Monitor: prove a resource is actually idle
Every resize or shutdown in this audit should survive 30 days of real metrics before anyone acts on it. Azure Monitor is that evidence layer, and az monitor metrics list is the fastest way to pull it (Microsoft Learn: az monitor metrics).
Grab the resource ID of a suspect VM:
az vm show --resource-group my-rg --name my-vm --query id --output tsv
Then pull a month of CPU at hourly resolution:
az monitor metrics list \
--resource "/subscriptions/<subscription-id>/resourceGroups/<rg>/providers/Microsoft.Compute/virtualMachines/<vm-name>" \
--metric "Percentage CPU" \
--start-time 2026-09-01T00:00:00Z \
--end-time 2026-10-01T00:00:00Z \
--interval PT1H \
--aggregation Average Maximum \
--output table
You get one row per hour with that hour's average and maximum CPU. Three shapes cover almost every VM you'll look at:
01Avg under 10%, max under 40%
Oversized
Real headroom. One size step down in the same family is a low-drama change.
02Low avg, max above 80%
Bursty
Downsizing hurts at the worst moment. Look at the B-series instead.
03Flat near zero
Abandoned
Not a sizing question. Find the owner or the half-finished teardown ticket.
Guest-level memory metrics need the Azure Monitor agent. If you haven't deployed it, CPU plus network is your honest signal. Say so in the findings doc instead of guessing at memory pressure.
The same command audits leak five. Point --resource at an Azure SQL database ID (/subscriptions/.../providers/Microsoft.Sql/servers/<server>/databases/<db>) and swap the metric: cpu_percent for vCore databases, dtu_consumption_percent for DTU ones. A database that never pushes past 40% for a month is provisioned at least one tier too high, and because Azure SQL tier changes are online operations, these findings carry the least remediation risk in the audit. Check the maximums too: a nightly index rebuild lives in the max column.
az CLI sweeps: the inventory Advisor never runs
The remaining leaks (orphaned disks, stale snapshots, unassociated IPs, questionable load balancers and Application Gateways) don't appear in any portal recommendation. Finding them is plain az CLI cost analysis: list resources in a known-wasteful state and turn each row into a ticket. The JMESPath filters below make the output an action list, not raw JSON to grep.
Unattached managed disks (leak one). Deleting a VM doesn't delete its disks unless deletion was explicitly configured, and an unattached disk bills at its full provisioned rate (Microsoft Learn: az disk):
az disk list \
--query "[?diskState=='Unattached'].{Name:name, RG:resourceGroup, SizeGB:diskSizeGb, Sku:sku.name}" \
--output table
Look at the SKU column first: an unattached Premium SSD is the most expensive kind of nothing Azure sells. Snapshot each disk, then delete it. The snapshot costs a fraction of the disk and keeps your rollback story.
Snapshots older than your retention policy (also leak one). List them oldest first:
az snapshot list \
--query "sort_by([], &timeCreated)[].{Name:name, RG:resourceGroup, SizeGB:diskSizeGb, Created:timeCreated}" \
--output table
Snapshots are individually cheap and collectively not. Anything older than your retention cutoff (90 days is a common default) with no documented reason gets deleted. Anything with a reason gets that reason written down.
Unassociated public IPs (leak four). A Standard public IP bills whether or not anything answers on it:
az network public-ip list \
--query "[?ipConfiguration==null].{Name:name, RG:resourceGroup, Sku:sku.name}" \
--output table
The per-IP cost is small. The finding matters because each orphaned IP marks a teardown that stopped halfway, so expect its siblings (disks, NICs, NSGs) in the same resource group.
VM power state (leaks two and seven, with an Azure-specific trap). A VM stopped from inside the guest OS reports VM stopped and keeps billing for compute. Only VM deallocated releases the hardware and stops the meter:
az vm list -d \
--query "[?powerState=='VM stopped'].{Name:name, RG:resourceGroup, Size:hardwareProfile.vmSize}" \
--output table
Every row here pays full compute price to do nothing. Run az vm deallocate on it or delete it, and remember that a deallocated VM still pays for its disks and static IPs, so the earlier sweeps apply to it too. While you're in this dataset, swap the filter to 'VM running' and cross-reference your environment tag: every non-production VM running at 2 a.m. is leak seven with a name and a resource group.
Load balancers and Application Gateways (leak four). There's no idle flag to query, so enumerate first:
az network lb list \
--query "[].{Name:name, RG:resourceGroup, Sku:sku.name}" --output table
az network application-gateway list \
--query "[].{Name:name, RG:resourceGroup, State:operationalState, Tier:sku.tier}" \
--output table
Then verify suspects with the Azure Monitor pattern above: --metric "ByteCount" for a load balancer, --metric "TotalRequests" for an Application Gateway, over 14 days. Application Gateways come first. They carry a meaningful fixed hourly charge, so one idle gateway outcosts a dozen orphaned IPs.
Run everything per subscription. All of the above is scoped to the active subscription, and waste concentrates in exactly the subscriptions nobody looks at. Loop the sweep:
for sub in $(az account list --query "[?state=='Enabled'].id" --output tsv); do
az account set --subscription "$sub"
echo "=== $(az account show --query name --output tsv) ==="
az disk list --query "[?diskState=='Unattached'].name" --output tsv
done
Add the other queries inside the loop. The dusty sandbox subscription from 2023 is usually your best hunting ground.
What the native audit covers, and what it doesn't
| Native audit | What it leaves to you | |
|---|---|---|
| Finding the leaks | All seven, across four tools | Running the four tools in the right order |
| Proving a resource is idle | 30 days of Azure Monitor metrics | Reading the max column and the calendar |
| Context | Tags, if you have them | Knowing it's the quarterly batch job |
| Cadence | Whenever someone runs it | Remembering to, every month |
| Fixes | None. Every tool reports or lists | Tickets, owners, change windows |
The deeper difference is between a snapshot and a process. Every tool in this article answers "what's wasteful right now?" accurately. None of them keeps asking, and none of them acts on the answer.
01Freshness
Expires on finish
Wednesday's deploys start recreating the waste you audited on Tuesday.
02Cost
Payroll, monthly
An afternoon every month from an engineer whose alternative was shipping.
03Evidence
Rots in the queue
A resize ticket built on June's metrics lands in an August sprint.
04Action
Remediates nothing
Every tool here reports, recommends or lists. Every fix is still a ticket.
It expires the moment you finish. You audited a Tuesday. Wednesday's deploys start recreating unattached disks, untagged resources and idle VMs, because each of the seven leaks is a byproduct of normal engineering work, not a one-time mess.
It has a payroll cost that compounds. The first pass takes an afternoon. Keeping it monthly takes an afternoon every month, and that trade gets renegotiated every sprint. The audit usually loses.
The evidence rots faster than the tickets move. A rightsizing finding built on June's metrics describes June. If the ticket lands in an August sprint, you're resizing on a two-month-old picture of the workload. Sometimes fine, sometimes a paged on-call engineer.
Nothing above remediates anything. Cost Management reports, Advisor recommends, Monitor measures and the CLI lists. None of them deletes a disk, deallocates a VM or buys a reservation. The gap between audited savings and realized savings is exactly the set of tickets that never found an owner.
What comes next
The fix for all four is the same: stop treating the audit as an event. Part three runs these exact checks continuously with read-only Azure access, investigates each finding against the context the native tools lack, and puts every fix behind your approval.
Related reading
- Azure cost optimization: 7 places your bill is leaking
- How to audit your AWS costs with native tools
- How to audit your GCP costs with native tools
- Manual vs continuous rightsizing
Or see it against your own subscriptions today: CloudThinker's free tier includes 100 premium credits, no card required.
