How To

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

Part one of the Azure cost series: the seven places Azure bills leak, grouped into three mechanisms (outlived resources, launch-day sizing, unmanaged meters), each with an az CLI command or portal path to find it and a typical recovery range. Ends on why monthly reviews can't keep up with waste that's generated every day.

11 min read
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 called rg-migration-temp, and nobody ever connects the two.

That's why Azure cost optimization is a different problem than most teams expect. The waste rarely sits 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 to $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.

The seven leaks come from three mechanisms. Knowing which one you're looking at tells you how risky the fix is.

01Leaks 1 and 4

Outlived resources

Disks, snapshots, public IPs and gateways that kept billing after the thing they served was deleted.

02Leaks 2 and 5

Launch-day sizing

VMs and databases sized for a traffic guess nobody revisited, billed by SKU instead of use.

03Leaks 3, 6 and 7

Unmanaged meters

List-price compute, cross-region bytes and dev boxes running all weekend, none of them raising an error.

Seven leaks, three mechanisms. Each section below has a command or portal path that finds it.

1. Unattached managed disks and orphaned snapshots

The leak. Deleting a VM in Azure doesn't 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 and bill at the full provisioned rate. A P30 (1 TiB Premium SSD) runs roughly $135 a month with zero readers. Snapshots compound it: 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 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 snapshots, list them oldest first:

az snapshot list \
  --query "sort_by([], &timeCreated)[].{Name:name, RG:resourceGroup, SizeGb:diskSizeGb, Created:timeCreated}" \
  -o table

Everything at the top of that list that predates your retention policy needs an owner or a deletion. Microsoft's walkthrough on finding unattached disks covers scripted cleanup, and switching recurring backups to incremental snapshots shrinks the cost of the ones you keep.

Typical range. 1 to 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, and dropping one size within the family roughly halves its cost. For a fleet 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 to 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: 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 and expiry per reservation. Then open Cost Management → Cost analysis, switch to Amortized cost, and group by Pricing model to see what share of compute runs on demand versus committed. A healthy target is 70 to 85% coverage of your stable baseline, with the rest left on demand for elasticity.

Typical range. Often the single largest item on this list: 10 to 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 a month whether or not anything uses it. A Standard Load Balancer runs roughly $18 a month at zero traffic. The expensive version is Application Gateway: a Standard_v2 gateway carries a fixed charge of roughly $0.25 per gateway-hour, about $180 a 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 on 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 in Azure Monitor. An empty backend pool, or byte counts flat at zero, means the resource is decorative.

Typical range. Usually $50 to $500 a month for IPs and load balancers, but each forgotten Application Gateway adds $180 or more on its own. Zero removal risk, and a reliable hint about 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 over $1,000 a month before storage. If it averages 6% CPU, you're paying for four databases and using one. The same 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. The change-window excuse is 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 and test databases, the serverless tier auto-pauses when idle and usually beats any hand-rolled schedule.

Typical range. Databases are often 15 to 30% of a SaaS company's Azure bill, with 20 to 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 resource responsible.

Find it. In the portal, open Cost Management → Cost analysis, group by Meter subcategory, and look for inter-region transfer and data-processed lines. To test whether a NAT gateway is the source, 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, and service endpoints or Private Link take it off the NAT data-processing meter entirely.

Typical range. Near zero for single-region monoliths, 5 to 15% of the bill for chatty multi-region architectures. If you also run AWS, the same meter blindness exists there, and 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 on 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's opt-in, per VM, and only handles the evening half. 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 with 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 to 40% of a mid-market bill, and scheduling cuts that portion by half or more.


The seven checks on one page

LeakWhere to lookTypical rangeRemoval risk
Unattached disks and snapshotsaz disk list, az snapshot list1 to 3% of the billNone
Idle and oversized VMsAzure Monitor CPU, Advisor10 to 20% of computeReboot
Reservation and savings plan gapsCost analysis by pricing model10 to 25% of computeCommitment
Public IPs, load balancers, App Gatewaysaz network ... list, throughput metrics$50 to $500/month, more per gatewayNone
Over-provisioned Azure SQLcpu_percent, dtu_consumption_percent20 to 40% of database spendLow (online)
Bandwidth and NAT processingCost analysis by meter subcategory5 to 15% in multi-region setupsArchitecture change
Non-prod running 24/7az vm list -d, environment tagHalf of non-prod computeLow

The order you fix them in matters more than the order they're listed:

01Start here

No workload attached

Unattached disks, unassociated IPs, empty backend pools. Nothing reads them, so removing them breaks nothing.

02Then

Reversible changes

Non-prod shutdown schedules and online Azure SQL tier changes. Easy to undo if a number was wrong.

03Plan it

Needs analysis

VM resizes, reservation purchases and bandwidth topology. Bigger money, but each one needs evidence first.

Order the work by removal risk, not by dollar size. The safe rows fund the time for the careful ones.

Why monthly Azure cost optimization reviews fall behind

Total the ranges above and a realistic mid-market subscription carries 20 to 35% addressable waste. The instinctive response is a monthly review meeting. It won't hold, for a structural reason: these seven sources aren't a backlog you burn down once. They're 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.

01Every day

Normal work

A VM gets deleted, a pipeline moves region, someone clones a dev environment.

02Same day

Waste starts

A disk is orphaned, a coverage gap opens, a schedule is never set.

03Month end

Review

Each finding is about two weeks old on average before anyone sees it.

04Next sprint

Maybe fixed

Findings queue behind feature work, and release-heavy months skip the review.

The waste is generated continuously. A monthly review samples it twelve times a year.

Review monthly and each finding is, on average, about 15 days old before anyone 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, and those are the months that generate the most waste. To reduce your Azure bill for good, detection has to run at the same cadence the waste does.

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 turns them into a repeatable audit built entirely on native tools: Cost Management, Azure Advisor, Azure Monitor and the az CLI.

For the ongoing half, CloudThinker Optimize connects to your subscription with read-only access and runs every check in this article continuously, so findings show up the day they appear instead of at month-end. Teams that work the full list typically cut 30 to 50% from their Azure bill. Part three shows how that works, including the approval gate in front of every change.

Related reading

Start free at app.cloudthinker.io with 100 premium credits and no card required.

azurecostoptimizationfinops