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.
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
| Leak | Where to look | Typical range | Removal risk |
|---|---|---|---|
| Unattached disks and snapshots | az disk list, az snapshot list | 1 to 3% of the bill | None |
| Idle and oversized VMs | Azure Monitor CPU, Advisor | 10 to 20% of compute | Reboot |
| Reservation and savings plan gaps | Cost analysis by pricing model | 10 to 25% of compute | Commitment |
| Public IPs, load balancers, App Gateways | az network ... list, throughput metrics | $50 to $500/month, more per gateway | None |
| Over-provisioned Azure SQL | cpu_percent, dtu_consumption_percent | 20 to 40% of database spend | Low (online) |
| Bandwidth and NAT processing | Cost analysis by meter subcategory | 5 to 15% in multi-region setups | Architecture change |
| Non-prod running 24/7 | az vm list -d, environment tag | Half of non-prod compute | Low |
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.
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.
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
- Optimize: continuous cloud cost optimization
- Manual vs continuous rightsizing
- AWS cost optimization: 7 places your bill is leaking
- Auto Mode: how CloudThinker gates every change
Start free at app.cloudthinker.io with 100 premium credits and no card required.
