How To

Azure Cost Optimization: 7 Places Your Bill Is Leaking (and How to Find Them)

Azure cost optimization guide: 7 places your bill is leaking, with copy-pasteable az CLI commands to detect each leak and typical savings ranges.

·
azurecostoptimizationfinops
Cover Image for Azure Cost Optimization: 7 Places Your Bill Is Leaking (and How to Find Them)

Azure Cost Optimization: 7 Places Your Bill Is Leaking (and How to Find Them)

A VM gets deleted in March. The 1 TiB Premium SSD that was attached to it keeps billing into November — eight months of charges for a disk nothing has read since the day its VM died. The invoice line says "Storage," the resource group is named rg-migration-temp, and nobody ever connects the two.

That pattern is what makes Azure cost optimization a different problem than most teams expect. The waste is rarely in the resources you watch. It hides in the ones that got decoupled from whatever you deleted, resized, or migrated away from — disks that outlive VMs, IPs that outlive gateways, reservations that lapse while usage keeps growing. The subscription keeps working, so nothing pages anyone.

On subscriptions running $10K–$500K a month, that decoupled waste concentrates in the same seven places. This guide walks through each one: what the leak is, why it survives, one command or exact portal path that exposes it, and the range you can expect to recover. Every command is copy-pasteable. Budget about an hour for the full sweep.

1. Unattached managed disks and orphaned snapshots

The leak. Deleting a VM in Azure does not reliably delete its storage. The portal's "Delete with VM" checkbox covers the OS disk if someone ticked it; data disks — and anything removed through the CLI, Bicep, or older automation — stay behind in the Unattached state, billing at the full provisioned rate. A P30 (1 TiB Premium SSD) runs roughly $135/month with zero readers. Snapshots compound the problem: every migration and every "back this up first" moment leaves a per-GB-month charge that never expires on its own.

Why it survives. Storage feels cheap and deletion feels dangerous, so orphaned disks in Azure default to living forever. Nothing alerts, nothing breaks, and the meter runs.

Find it. One query across the subscription:

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

Anything more than a month old with no attachment is a near-certain orphan — snapshot it if you want a safety net, then delete it. For the snapshot side, run az snapshot list -o table and sort by creation date; anything past your stated retention policy needs an owner or a deletion. Microsoft's walkthrough on finding unattached disks covers scripted cleanup, and converting recurring backups to incremental snapshots shrinks the cost of the ones you keep.

Typical range. 1–3% of the bill, and the least risky money in this article: an unattached disk has, by definition, no workload on it.

2. Idle and oversized VMs

The leak. VMs whose CPU has idled in single digits for a month straight. A few are abandoned experiments; most were sized "to be safe" at launch — a D8s v5 doing a D2s v5's job. Azure bills the SKU, not the utilization.

Why it survives. Rightsizing means a reboot, a reboot means a change window, and the person who picked the original size left two reorgs ago. Launch-day guesses become permanent by default.

Find it. Pull 30 days of CPU for a suspect VM:

az monitor metrics list \
  --resource $(az vm show -g my-rg -n my-vm --query id -o tsv) \
  --metric "Percentage CPU" \
  --interval PT6H --offset 30d \
  --aggregation Average Maximum \
  -o table

An average under 10% with a maximum under 40% across the whole window makes the VM a resize candidate — dropping one SKU size roughly halves its cost. For a fleet-wide view instead of one VM at a time, Azure Advisor cost recommendations flag underutilized VMs from your own telemetry; az advisor recommendation list --category Cost -o table prints them in one shot.

Typical range. 10–20% of compute spend on subscriptions that never built a rightsizing habit.

3. Azure reservations and savings plan coverage gaps

The leak. Every hour of steady baseline compute paid at pay-as-you-go rates is a discount you declined. Azure reservations discount VMs by up to 72% against on-demand pricing; an Azure savings plan for compute trades some of that discount (up to 65%) for flexibility across VM families and regions. Under-commitment is the quiet failure. The loud one is a three-year reservation expiring with no renewal alert configured — coverage drops to zero overnight and the bill jumps with no code change anywhere.

Why it survives. Commitment feels like risk, so teams hedge by buying nothing, then run the same D-series baseline for three years at list price. Growth erodes coverage from one side, expiries from the other, and neither produces an error message.

Find it. In the portal: Cost Management + Billing → Reservations shows utilization per reservation. Then open Cost Management → Cost analysis, switch to the amortized cost view, and group by pricing model to see what share of compute runs on-demand versus committed. A healthy target is 70–85% coverage of your stable baseline, with the remainder left on-demand for elasticity.

Typical range. Often the single largest item in this list — 10–25% of compute spend where commitments were never systematized.

4. Unused public IPs and idle load balancers

The leak. A standard public IP bills about $3.65/month whether or not anything uses it. A Standard Load Balancer runs roughly $18/month at zero traffic. The expensive version of this leak is Application Gateway: a Standard_v2 instance carries a fixed charge of roughly $0.25 per gateway-hour — about $180/month — before it processes a single request (Application Gateway pricing).

Why it survives. Network plumbing gets created per experiment and per migration, then outlives whatever it fronted. An idle Application Gateway looks exactly like a working one from every dashboard you normally check.

Find it. Unassociated public IPs across the subscription:

az network public-ip list \
  --query "[?ipConfiguration==null].{Name:name, RG:resourceGroup, Method:publicIPAllocationMethod, Sku:sku.name}" \
  -o table

Then list the rest of the plumbing with az network lb list -o table and az network application-gateway list -o table, and check each one's backend pool plus two weeks of throughput metrics in Azure Monitor. An empty backend pool, or byte counts flat at zero, means the resource is decorative.

Typical range. Usually $50–500/month for IPs and load balancers, but each forgotten Application Gateway adds $180 or more on its own. Zero removal risk, and a reliable indicator of how much other hygiene debt the subscription carries.

5. Over-provisioned Azure SQL and managed databases

The leak. Database tiers get chosen once, under launch-day uncertainty, by whoever felt most nervous — and a production database that works never gets touched again. A General Purpose 8-vCore Azure SQL database costs well north of $1,000/month before storage; if it averages 6% CPU, you are paying for four databases and using one. The same pattern holds for Flexible Server PostgreSQL and MySQL.

Why it survives. Fear of the resize — even though scaling Azure SQL resources is an online operation for most tier changes, which makes the change-window excuse far weaker here than it is for VMs.

Find it. Pull two weeks of CPU — cpu_percent on the vCore model, dtu_consumption_percent on DTU tiers:

az monitor metrics list \
  --resource $(az sql db show -g my-rg -s my-server -n my-db --query id -o tsv) \
  --metric cpu_percent \
  --interval PT1H --offset 14d \
  --aggregation Average Maximum \
  -o table

An average under 20% with peaks under 60% is a strong down-tier signal; check memory and session metrics too before you move. For dev/test databases, the serverless tier auto-pauses when idle and usually beats any hand-rolled schedule.

Typical range. Databases are often 15–30% of a SaaS company's Azure bill, with 20–40% recoverable inside that line alone.

6. Bandwidth: cross-region traffic and NAT gateway processing

The leak. Two meters nobody models at design time. Traffic between Azure regions bills per GB — roughly $0.02/GB and up depending on the continent (bandwidth pricing) — so geo-replication, cross-region service calls, and a "temporary" pipeline reading a storage account in another region all accrue quietly. NAT gateway adds about $0.045 per GB processed on top of its hourly charge, and private subnets pulling container images or package feeds through it can move terabytes a month.

Why it survives. Nothing in a Bicep template says "this replication topology will move 20 TB a month between regions." The charges land under bandwidth meters that never name the culprit resource.

Find it. In the portal: Cost Management → Cost analysis, group by Meter subcategory, and look for inter-region data transfer and data-processed lines. To test whether a NAT gateway is the offender, pull its processed bytes:

az monitor metrics list \
  --resource $(az network nat gateway show -g my-rg -n my-natgw --query id -o tsv) \
  --metric ByteCount --interval PT6H --offset 7d \
  --aggregation Total \
  -o table

Then ask what that traffic actually is. Azure-service traffic taking the public path is the classic finding — service endpoints or Private Link takes it off the NAT data-processing meter entirely.

Typical range. Near zero for single-region monoliths; 5–15% of the bill for chatty multi-region architectures. If you also run AWS, the same meter blindness exists there — the AWS version of this guide covers the cross-AZ and NAT gateway equivalents.

7. Non-production environments running 24/7

The leak. A dev or QA environment used 12 hours a day, five days a week, needs 60 of the week's 168 hours. Left running around the clock, roughly two-thirds of its compute cost buys nothing. Multiply that across every dev, staging, demo, and load-test VM in the subscription.

Why it survives. Azure ships auto-shutdown on every VM, but it is opt-in, per-VM, and only handles the evening half of the problem — someone still has to start machines in the morning, and one broken Monday standup usually ends the initiative.

Find it, then fix it in one command. Run az vm list -d -o table late on a Sunday and read the power-state column: every running VM with dev, test, or staging in its name is burning weekend hours. Schedule the shutdown half on the spot:

az vm auto-shutdown -g rg-dev -n vm-dev-01 --time 1900

The time is UTC. If you tag environments, group Cost analysis by your environment tag and compare non-production spend against what a 60-hour week should cost. If you don't tag environments, fix that first — tagging gates every other check in this article.

Typical range. Non-production commonly runs 20–40% of a mid-market bill; scheduling cuts that portion by half or more.

Why monthly Azure cost optimization reviews fall behind

Total the ranges above and a realistic mid-market subscription is carrying 20–35% addressable waste. The instinctive response is a monthly review meeting. It won't hold, for a structural reason: these seven sources are not a backlog you burn down once. They are processes that generate waste continuously. Every VM deletion can orphan a disk. Every quarter of growth reopens a reservation coverage gap. Every new hire clones a dev environment that nobody schedules.

Review monthly and each finding is, on average, about 15 days old before anyone even sees it — and older still before anyone acts, because findings queue behind feature work. The review itself costs an engineer most of a day, which is exactly why it gets skipped in release-heavy months, which are exactly the months that generate the most waste. To reduce your Azure bill durably, detection has to run at the same cadence the waste does: continuously.

Make Azure cost optimization continuous

The first pass through these seven checks takes about an hour and usually pays for itself the same day. Part two of this series turns them into a repeatable audit built entirely on native tools — Cost Management, Azure Advisor, and the az CLI.

For the ongoing half, CloudThinker's CostOps module connects to your subscription with read-only access and runs every check in this article continuously, surfacing findings the day they appear instead of at month-end. Teams that work the full list typically cut 30–50% from their Azure bill. Start free at app.cloudthinker.io — 100 premium credits, no card required.