How To

How to Audit Your Azure Costs with Native Tools (Cost Management, Advisor, Azure Monitor)

Run a complete Azure cost audit with native tools: Cost Management, Advisor, Azure Monitor, and az CLI sweeps covering all seven waste sources.

·
azurecostoptimizationfinops
Cover Image for How to Audit Your Azure Costs with Native Tools (Cost Management, Advisor, Azure Monitor)

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 fronting 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.

This is part two of a three-part series. 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, but grouped by tool rather than by waste source, because that is how the work actually flows: you open one console blade or one terminal and harvest everything it can tell you before moving on. Plan for three to four focused hours on a first pass across a handful of subscriptions.

One prerequisite before you start: 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 — confirm scope first with az account list --output table.

Cost Management + Billing: read the bill before touching a single resource

Everything in this section lives at Cost Management + Billing → Cost Management → Cost analysis in the Azure portal (Microsoft Learn: quickstart). Resist the urge to jump straight to deleting disks. Ten minutes here decides which of the seven waste sources 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:

  1. Group by: Service name. This is your priority list. If Virtual Machines, Storage, and Azure SQL together carry most of the bill — the usual pattern at a mid-market SaaS — then VM rightsizing, disk hygiene, and SQL tiers are where the hours below pay off. If Bandwidth or a data service dominates instead, reweight accordingly.
  2. Group by: Resource group. Sort descending and scan for names you cannot explain: old project codenames, a departed engineer's initials, anything suffixed -test or -poc that is somehow still accruing. Each unexplained resource group is a pre-made audit finding; write it down now and chase ownership later.
  3. Group by: Tag, keyed on environment (or your equivalent). This measures waste source 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. And check the untagged slice — if it is a double-digit percentage of spend, poor tagging is itself a finding, because it caps how precise every future azure cost audit can be.

For the bandwidth line (waste source 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 is no universal healthy number here, so audit the trend instead: bandwidth growing faster than request volume usually means chatty cross-region replication or traffic taking a paid path (through a public endpoint) 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 + Billing → Cost Management → Budgets and create one budget per subscription (Microsoft Learn: create budgets). Set the amount to last month's actuals plus your expected growth, with alerts at 80% and 100% of Actual cost and one at 100% of Forecasted cost. Budgets remediate nothing — they are tripwires — but the forecasted alert in particular converts "we discovered the overrun at invoice time" into an email mid-month, while the spend is still stoppable.

Reservation and savings plan coverage

Waste source three hides in plain sight in Cost analysis. Change Group by to Pricing model and switch the cost 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–85% commitment coverage of that baseline, with the remainder on demand for elasticity and churn. Materially below that, you are paying list price for capacity whose usage you could predict; Microsoft's published reservation discounts run as deep as 72% for three-year terms (Microsoft Learn: reservations), so the gap is rarely small money. Do not chase 100% — every point of coverage above your true baseline becomes a commitment you must feed.

Coverage is only half the ledger. Open Cost Management + Billing → Reservations and check each reservation's utilization percentage (Microsoft Learn: reservation utilization). Utilization persistently below the mid-90s means an earlier commitment outlived its workload; instance size flexibility within the VM family may re-absorb it, and exchanging the reservation beats buying more coverage on top of a leak. Read coverage and utilization together before any purchase decision — buying coverage while existing reservations sit half-used just moves the waste.

Azure Advisor: recommendations you already have, read correctly

Azure Advisor's Cost category is the closest thing to a free pre-run audit: continuously generated, zero setup, already waiting in your portal at Advisor → Cost (Microsoft Learn: Advisor cost recommendations). The CLI equivalent:

az advisor recommendation list --category Cost --output table

Here is how the azure advisor recommendations map onto the waste sources from part one:

Advisor recommendation Waste source 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

Two things to understand before treating this list as your findings document.

The savings estimates are directional, not exact. Each recommendation carries an estimated annual savings figure. It is computed from retail rates and recent usage, and it does not know about 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 is trustworthy even when the absolute dollars are not — and never paste them into a business case as committed savings.

The VM check is deliberately conservative. By default, "underutilized" means CPU at or below 5% over the trailing 7 days. A VM idling at 12% CPU is still mostly wasted, but Advisor stays silent 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 pending verification — a 7-day lookback cannot see a monthly close process or a quarterly report job, which is exactly what the Azure Monitor pass below is for.

Notice what the table does not contain: unattached disks, aging snapshots, orphaned public IPs, over-provisioned Azure SQL tiers, bandwidth, and non-prod scheduling are all absent or only partially covered. Advisor is a useful start and a dangerously incomplete finish — roughly half of part one's waste sources never appear in it, which is why the CLI section below exists.

For the findings document, use Download as CSV on the Advisor recommendations page (or pipe the CLI output to a file). Capture the export date in the filename; you will care about staleness later.

Azure Monitor: prove a resource is actually idle

Every resize or shutdown decision in this audit should survive contact with 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).

First 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 with hourly resolution:

az monitor metrics list \
  --resource "/subscriptions/<subscription-id>/resourceGroups/<rg>/providers/Microsoft.Compute/virtualMachines/<vm-name>" \
  --metric "Percentage CPU" \
  --start-time 2026-06-13T00:00:00Z \
  --end-time 2026-07-13T00:00:00Z \
  --interval PT1H \
  --aggregation Average Maximum \
  --output table

Interpreting the output. You get one row per hour with the average and maximum CPU for that hour. The pattern that clears a VM for rightsizing: average consistently under about 10% and hourly maximums under about 40% across the full 30 days. That resource has genuine headroom, and one size step down within the same family is a low-drama change. Two other patterns matter:

  • Low average, occasional maximums above 80% — a bursty workload. Downsizing will hurt at exactly the wrong moment; evaluate the burstable B-series instead, which prices for the idle baseline while banking credits for the spikes.
  • Flat near-zero for the entire window — this VM is not oversized, it is abandoned. The question is no longer "which size" but "why does this exist", and the answer usually involves waste source seven or a deprovisioning ticket someone half-finished.

Guest-level memory metrics require the Azure Monitor agent, so if you have not deployed it, CPU plus network is your honest signal — say so in the findings doc rather than guessing at memory pressure.

The same command audits waste source 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-model ones. A database that never pushes utilization 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 of anything in the audit. Verify the maximums, not just the average: a nightly index rebuild lives in the max column.

az CLI sweeps: the inventory Advisor never runs

The remaining waste sources — orphaned disks, stale snapshots, unassociated IPs, questionable load balancers and Application Gateways — do not appear in any portal recommendation. Finding them is plain az CLI cost analysis: enumerate resources in a known-wasteful state and turn each row into a ticket. The queries below use JMESPath filters so the output is already an action list, not raw JSON to grep.

Unattached managed disks (waste source one). Deleting a VM in Azure does not delete its disks unless deletion was explicitly configured, and an unattached disk bills at its full provisioned rate regardless of contents (Microsoft Learn: az disk):

az disk list \
  --query "[?diskState=='Unattached'].{Name:name, RG:resourceGroup, SizeGB:diskSizeGb, Sku:sku.name}" \
  --output table

Sort mentally by SKU: 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 preserves your rollback story.

Snapshots older than your retention policy (also waste source one). Set the date to your actual retention cutoff; 90 days is a common default:

az snapshot list \
  --query "[?timeCreated<='2026-04-13'].{Name:name, RG:resourceGroup, SizeGB:diskSizeGb, Created:timeCreated}" \
  --output table

Snapshots are individually cheap and collectively not. Anything predating the cutoff with no documented reason to exist gets deleted; anything with a reason gets that reason written down.

Unassociated public IPs (waste source four). A Standard SKU 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 — expect to find its siblings (disks, NICs, NSGs) in the same resource group.

VM power state (waste sources two and seven, Azure-specific trap included). A VM stopped from inside the guest OS reports VM stopped and continues to bill 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 is paying full compute price to do nothing. Run az vm deallocate on it or delete it — and remember a deallocated VM still pays for its disks and static IPs, so the earlier sweeps apply to it too. While you are in this dataset, swap the filter to 'VM running' and cross-reference against your environment tag: every non-production VM running at 2 a.m. is waste source seven with a name and a resource group.

Load balancers and Application Gateways (waste source four). There is no state=='idle' field 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 from the previous section — --metric "ByteCount" for a load balancer, --metric "TotalRequests" for an Application Gateway, over 14 days. Application Gateways deserve priority: they carry a meaningful fixed hourly charge, so an idle one 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 whole 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

Substitute or append the other queries inside the loop. The dusty sandbox subscription from 2023 is statistically your best hunting ground.

What this audit will not do for you

Run the audit — the findings document at the end of it is real money. But be clear-eyed about four properties of the exercise, because they determine whether it changes your bill once or changes it durably.

It expires the moment you finish it. You have audited a Tuesday. Wednesday's deploys begin recreating unattached disks, untagged resources, and idle VMs immediately, because each of the seven waste sources 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, from an engineer whose alternative was shipping. That trade gets renegotiated every sprint, and the audit usually loses.

The evidence rots faster than the tickets move. A rightsizing finding built on June's metrics describes June. If the remediation ticket lands in an August sprint, you are resizing a workload based on a two-month-old snapshot of its behavior — sometimes fine, sometimes a paged on-call engineer.

Nothing above remediates anything. Cost Management reports, Advisor recommends, Monitor measures, and the CLI enumerates — not one of them deletes a disk, deallocates a VM, or buys a reservation. Every finding becomes a ticket, and 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 limits is the same: stop treating the audit as an event. Part three covers running these exact checks continuously with an AI agent — read-only Azure access, findings that never go stale, remediation gated behind your approval.

Or see it against your own subscriptions today: CloudThinker's free tier includes 100 premium credits, no card required.