CloudFinOps logo
CloudFinOps
Azure

Azure Idle Resources: How to Find and Eliminate Wasted VMs, Disks & IPs (2026)

Nishant Jain

Nishant Jain

Co-Founder & CTO · · 13 min read

The Azure Waste Problem

Azure makes it incredibly easy to provision resources — and equally easy to forget about them. A developer spins up a D4s_v3 VM for a quick test, forgets to deallocate it, and you're paying $140/month for a machine doing nothing.

According to Microsoft's own data, the average Azure subscription has 15-25% waste in idle or over-provisioned resources. For a team spending ₹20L/month on Azure, that's ₹3-5L/month burning away.

Unlike AWS where "stopping" an instance stops billing for compute (but not EBS), Azure's "Stopped" state is different from "Stopped (deallocated)." A VM in "Stopped" state still incurs compute charges. Only "Stopped (deallocated)" stops the billing clock. This distinction alone accounts for thousands in unnecessary Azure spend.

The Five Types of Azure Waste

Resource TypeHow It Becomes WasteMonthly Cost Example
Idle VMsTest/dev machines left running 24/7D4s_v3: ~$140/month
Orphaned Managed DisksVM deleted but disk retainedP30 (1TB): ~$122/month
Unattached Public IPsReserved but not assigned to running resources~$3.65/month each (adds up)
Idle App Service PlansPlan running but no apps deployedS1: ~$73/month
Over-provisioned databasesAzure SQL provisioned for peak, idle 90% of timeS3 (100 DTUs): ~$150/month

Step 1: Use Azure Advisor for Quick Wins

Azure Advisor is your free first line of defense. It analyzes your resource configuration and telemetry to give personalized recommendations.

Accessing Cost Recommendations

# List all cost recommendations via Azure CLI az advisor recommendation list --category Cost --output table # Filter for high-impact items az advisor recommendation list --category Cost \ --query "[?impact=='High']" --output table

Azure Advisor specifically flags:

  • Idle VMs: CPU average <5% over 14 days
  • Rightsizing opportunities: VMs that could use a smaller SKU
  • Reserved Instance recommendations: Stable workloads that would benefit from commitments
  • Unused Public IPs: IPs not attached to any NIC

Limitations of Azure Advisor

Advisor is a starting point, not a complete solution:

  • It uses 14-day lookback — misses weekly patterns (a VM idle on weekends isn't truly idle)
  • It doesn't detect orphaned disks directly
  • It doesn't consider the relationship between resources (a "idle" VM might be a standby for failover)
  • Recommendations are generic — no CLI commands to execute fixes

See how much you're wasting

Get a free 7-day cloud audit. No credit card, no agents, read-only access.

Start Free Audit

Step 2: Find Idle Virtual Machines

Define Your Idle Threshold

A VM is "idle" when it meets ALL of these criteria over a 30-day window:

MetricIdle ThresholdAzure Monitor Metric Name
CPUAverage <5%Percentage CPU
Network In<1 MB/dayNetwork In Total
Network Out<1 MB/dayNetwork Out Total
Disk Read Ops<100/dayDisk Read Operations/Sec
Disk Write Ops<100/dayDisk Write Operations/Sec

Azure CLI: Find Low-CPU VMs

# Get all running VMs in a subscription az vm list --query "[?powerState=='VM running']" \ -d --output table # Check CPU metrics for a specific VM (last 30 days) az monitor metrics list \ --resource "/subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{vm-name}" \ --metric "Percentage CPU" \ --interval PT1H \ --start-time $(date -u -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ) \ --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \ --aggregation Average \ --output table

Azure Resource Graph: Bulk Query for Idle VMs

For subscriptions with hundreds of VMs, use Resource Graph:

# Find all running VMs with their size and resource group az graph query -q " Resources | where type == 'microsoft.compute/virtualmachines' | where properties.extended.instanceView.powerState.code == 'PowerState/running' | project name, resourceGroup, location, vmSize = properties.hardwareProfile.vmSize, subscriptionId | order by vmSize desc "

Cross-reference this with Azure Monitor metrics to identify the idle subset.

Step 3: Find Orphaned Managed Disks

When you delete a VM, Azure doesn't automatically delete its managed disks. These "orphaned" disks sit in your subscription accumulating charges.

# Find all managed disks NOT attached to any VM az disk list --query "[?managedBy==null]" \ --output table \ --query "[].{Name:name, ResourceGroup:resourceGroup, Size:diskSizeGb, SKU:sku.name, State:diskState}"

Expected output: A list of disks with diskState: Unattached. Each of these is costing you money for storage that nothing is using.

Cost impact by disk tier:

Disk SKUSizeMonthly Cost (Unattached)
Standard HDD (S10)128 GB~$5.89
Standard SSD (E10)128 GB~$9.60
Premium SSD (P30)1 TB~$122.88
Premium SSD (P50)4 TB~$491.52

A common pattern: a team creates 10 VMs for load testing, each with a 1TB Premium SSD. They delete the VMs but not the disks. That's $1,228/month in orphaned storage — $14,740/year — for zero value.

Safe Cleanup Process

  1. Snapshot first (if the data might be needed):
az snapshot create --resource-group {rg} \ --name {disk-name}-snapshot \ --source {disk-id}
  1. Verify no dependency (check if any recovery is pending):
az disk show --ids {disk-id} --query "diskState" # Should return "Unattached" — not "ReadyToUpload" or "ActiveSAS"
  1. Delete the orphaned disk:
az disk delete --ids {disk-id} --yes

Step 4: Find Unattached Public IP Addresses

Public IPs in Azure cost money whether they're attached to a resource or not (for Standard SKU). Basic SKU IPs are free when attached but cost money when not.

# Find all Public IPs not attached to anything az network public-ip list \ --query "[?ipConfiguration==null]" \ --output table \ --query "[].{Name:name, ResourceGroup:resourceGroup, SKU:sku.name, Address:ipAddress, Location:location}"

Each unattached Standard Public IP costs approximately $3.65/month. A subscription with 50 unattached IPs is wasting $182/month — not catastrophic individually, but this waste pattern signals broader hygiene issues.

Step 5: Detect Over-Provisioned Azure SQL Databases

Azure SQL databases provisioned in the DTU model often sit at 5-10% utilization:

# Get DTU usage percentage for the last 14 days az monitor metrics list \ --resource "/subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{server}/databases/{db}" \ --metric "dtu_consumption_percent" \ --interval PT1H \ --start-time $(date -u -d "14 days ago" +%Y-%m-%dT%H:%M:%SZ) \ --aggregation Average Maximum \ --output table

Decision framework:

Average DTU UsagePeak DTU UsageAction
<10%<30%Downgrade by 2 tiers
<10%30-60%Downgrade by 1 tier
10-40%<60%Downgrade by 1 tier
10-40%>60%Keep current tier
>40%>80%Consider upgrading

Step 6: Automate Detection with Azure Automation

Set up a recurring runbook that identifies waste automatically:

Create an Automation Account Runbook

# Azure Automation Runbook (Python) # Runs weekly, reports idle resources to a Teams webhook import automationassets from azure.identity import ManagedIdentityCredential from azure.mgmt.compute import ComputeManagementClient from azure.mgmt.monitor import MonitorManagementClient import requests import json from datetime import datetime, timedelta credential = ManagedIdentityCredential() subscription_id = automationassets.get_automation_variable("SubscriptionId") webhook_url = automationassets.get_automation_variable("TeamsWebhookUrl") compute_client = ComputeManagementClient(credential, subscription_id) monitor_client = MonitorManagementClient(credential, subscription_id) idle_vms = [] end_time = datetime.utcnow() start_time = end_time - timedelta(days=14) for vm in compute_client.virtual_machines.list_all(): if vm.instance_view and "running" in str(vm.instance_view): # Check CPU metrics metrics = monitor_client.metrics.list( vm.id, timespan=f"{start_time.isoformat()}/{end_time.isoformat()}", metricnames="Percentage CPU", aggregation="Average" ) for metric in metrics.value: for ts in metric.timeseries: avg_cpu = sum(d.average for d in ts.data if d.average) / len(ts.data) if avg_cpu < 5: idle_vms.append({ "name": vm.name, "rg": vm.id.split("/")[4], "size": vm.hardware_profile.vm_size, "avg_cpu": round(avg_cpu, 1) }) # Post to Teams if idle_vms: message = { "text": f"⚠️ Found {len(idle_vms)} idle VMs (CPU < 5% over 14 days):\n" + "\n".join(f"• {vm['name']} ({vm['size']}) in {vm['rg']} — {vm['avg_cpu']}% avg CPU" for vm in idle_vms) } requests.post(webhook_url, json=message)

Schedule this runbook to run weekly. It gives you a proactive Slack/Teams notification of waste without requiring anyone to log into the portal.

Step 7: Implement Continuous Governance

Azure Policy for Prevention

Prevent waste from accumulating in the first place:

{ "if": { "allOf": [ { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, { "field": "tags['team']", "exists": "false" } ] }, "then": { "effect": "deny" } }

This denies VM creation without a team tag — ensuring every resource has an accountable owner from day one.

Auto-Shutdown for Dev/Test

Azure has a built-in auto-shutdown feature for VMs:

# Enable auto-shutdown at 8 PM IST for a dev VM az vm auto-shutdown \ --resource-group dev-rg \ --name dev-vm-01 \ --time 2030 \ --timezone "India Standard Time"

For entire environments, use Azure DevTest Labs or simply schedule Start/Stop VMs during off-hours (a pre-built Azure Automation solution).

Tools for Azure Idle Resource Detection

ToolCostWhat It DetectsAutomation
Azure AdvisorFreeIdle VMs, rightsizing, RI recommendationsManual action
Azure Cost ManagementFreeBudget alerts, cost trendsAlert-based
CloudFinOpsFree auditIdle VMs, orphaned disks, governance gapsAI recommendations with CLI commands
Azure Automation~$2/500 minutesCustom detection logicFully automated
Azure Resource GraphFreeResource inventory queriesQuery-based

FAQ

How is "Stopped" different from "Stopped (deallocated)" in Azure?

"Stopped" means the OS shut down but Azure still reserves the compute capacity — you're still billed for the VM. "Stopped (deallocated)" releases the compute capacity back to Azure — billing stops for compute (but continues for attached disks and IPs). Always use az vm deallocate instead of just az vm stop to actually save money.

Will deleting an orphaned disk lose my data permanently?

Yes, unless you create a snapshot first. Snapshots cost much less than managed disks (incremental storage only) and can be used to recreate the disk later. Always snapshot before deleting if there's any chance the data is needed.

How much can I realistically save by cleaning up idle Azure resources?

Most teams find 15-25% savings in their first cleanup. For a ₹20L/month Azure subscription, that's ₹3-5L/month in immediate savings. The recurring savings depend on how well you prevent future waste through governance policies and automation.

Should I use Azure Reserved Instances or Savings Plans?

For predictable, stable workloads that won't change VM series: use Reserved Instances (higher savings, less flexibility). For workloads that might change size or series: use Azure Savings Plans (slightly lower savings, more flexibility). Most teams benefit from a mix: RIs for databases and production VMs, Savings Plans for variable compute.

How often should I run idle resource detection?

Weekly is the sweet spot. Daily creates alert fatigue (short-term spikes look like anomalies). Monthly is too slow — by the time you detect a forgotten VM, you've already paid for 4 weeks of waste. Weekly gives you a clean signal-to-noise ratio with fast enough response time.

Related articles