Cleaning Up Unused Azure Resources

Unused resources are a silent drain on Azure spend. Managed disks attached to deleted VMs, unattached public IP addresses, idle App Service Plans, and forgotten load balancers accumulate over time and collectively add up to a significant and entirely avoidable monthly bill.

Why unused resources accumulate

The most common cause is incomplete deletion. When a developer deletes a VM through the portal, they often delete only the VM resource itself. The associated managed disk, public IP, network interface, and network security group are left behind as orphaned resources. Each of these continues to appear on the monthly invoice.

Other common sources of waste include App Service Plans with no apps deployed, load balancers that were set up for a test environment and never torn down, old disk snapshots taken before a migration that was completed months ago, and resource groups created by experimentation that were never cleaned up.

The problem compounds in organisations where multiple teams deploy independently and there is no regular review process. A quarterly cleanup exercise in a mid-sized Azure environment typically finds hundreds of orphaned resources representing thousands of dollars per month.

Resource types that commonly go unused

Resource typeMonthly cost when idleHow it gets orphaned
Managed disk (128 GiB Premium SSD)~$20VM deleted but disk not deleted
Public IP address (Static)~$3.65NIC or load balancer deleted, IP left unassigned
App Service Plan (B1)~$13App deleted, plan left running
Standard Load Balancer~$18 base + data processingTest environment deleted, LB not removed
Disk snapshot (128 GiB)~$5–7Pre-migration snapshot never deleted
Virtual network gateway~$27+ (VpnGw1)VPN connection closed, gateway not deleted
Stopped VM (not deallocated)Full compute rateOS shutdown, not deallocated in Azure
Note

A static public IP address costs approximately $0.005/hour whether or not it is attached to anything. At scale, dozens of unattached static IPs add meaningful cost. Reserved (static) IPs have a higher unattached rate than dynamic IPs.

Finding orphaned resources with the CLI

The Azure CLI makes it straightforward to query for specific orphaned resource types across a subscription. The following commands identify the most common categories.

# Find all unattached managed disks
az disk list \
  --query "[?diskState=='Unattached'].{Name:name, ResourceGroup:resourceGroup, SizeGB:diskSizeGb, SKU:sku.name}" \
  --output table

# Find all public IPs not associated with any resource
az network public-ip list \
  --query "[?ipConfiguration==null].{Name:name, ResourceGroup:resourceGroup, AllocationMethod:publicIpAllocationMethod}" \
  --output table

# Find App Service Plans with zero apps
az appservice plan list \
  --query "[?numberOfSites==\`0\`].{Name:name, ResourceGroup:resourceGroup, SKU:sku.name, NumberOfSites:numberOfSites}" \
  --output table

# Find disk snapshots older than 90 days (replace date as needed)
az snapshot list \
  --query "[?timeCreated<'2025-12-19'].{Name:name, ResourceGroup:resourceGroup, SizeGB:diskSizeGb, Created:timeCreated}" \
  --output table

# Find load balancers with no backend pool members
az network lb list \
  --query "[?backendAddressPools[0].backendIPConfigurations==null].{Name:name, ResourceGroup:resourceGroup}" \
  --output table

Run these queries in a read-only context first to audit scope before taking any deletion action. Pipe results to a file for review if the list is long.

Azure Advisor cost recommendations

Azure Advisor actively identifies waste and surfaces it in the Cost section of the Advisor portal. It flags idle VMs (running below 5% CPU for 7 days), unattached public IPs, empty App Service Plans, and underused ExpressRoute circuits.

# Pull all Advisor cost recommendations
az advisor recommendation list \
  --category Cost \
  --output table

# Filter for specific resource types
az advisor recommendation list \
  --category Cost \
  --query "[?resourceMetadata.resourceType=='microsoft.compute/disks']" \
  --output table

Advisor recommendations include an estimated annual saving for each item. Sorting by impact (high/medium/low) lets you prioritise the highest-value cleanup targets first. Advisor does not catch everything — it focuses on resources it has direct visibility into — so combine it with the manual CLI queries above for full coverage.

Using Cost Analysis to identify waste by resource type

The Azure Cost Analysis portal lets you filter spend by resource type and group by resource name. To find high-cost resource types that might contain idle resources, open Cost Analysis, set the scope to your subscription, change the grouping to Resource Type, and sort by cost descending. Resource types with unexpectedly high spend relative to their business value are worth investigating further.

Filter by tag to identify resources that lack an owner tag — untagged resources are often orphaned. A department or team tag missing from a compute resource is a strong signal it was created informally and may no longer be needed.

Using Azure Policy to enforce lifecycle tagging

The most durable cleanup strategy is preventing orphaned resources in the first place. Azure Policy can require specific tags on resource creation, making it impossible to deploy a VM, disk, or public IP without attaching an owner and expiry-date tag.

# Assign a built-in policy requiring a specific tag on all resources
az policy assignment create \
  --name "require-owner-tag" \
  --policy "871b6d14-10aa-478d-b590-94f262ecfa99" \
  --params '{"tagName": {"value": "owner"}}' \
  --scope /subscriptions/{subscription-id}

With an expiry-date tag in place, you can write a cleanup script or Logic App that queries for resources with an expiry date in the past and either deletes them automatically or sends a notification to the resource owner to re-validate.

# Example: list all VMs with expiry-date tag before today's date
az vm list \
  --query "[?tags.\"expiry-date\" < '2026-03-19'].{Name:name, RG:resourceGroup, Expiry:tags.\"expiry-date\"}" \
  --output table
Tip

Use a 90-day re-validation cadence. Any resource that has not been explicitly re-tagged within 90 days becomes a deletion candidate. This shifts the default from “assume it is needed” to “prove it is needed.”

Automated cleanup scripts

For teams managing many subscriptions, manual CLI queries do not scale. An automated script run as an Azure Automation Runbook or a scheduled Logic App can regularly audit and report — or in mature environments, delete — orphaned resources.

A safe automated cleanup pattern for orphaned disks:

#!/bin/bash
# Identify unattached disks and delete them after confirmation
UNATTACHED=$(az disk list \
  --query "[?diskState=='Unattached'].id" \
  --output tsv)

for DISK_ID in $UNATTACHED; do
  DISK_NAME=$(az disk show --ids "$DISK_ID" --query name -o tsv)
  echo "Deleting orphaned disk: $DISK_NAME"
  az disk delete --ids "$DISK_ID" --yes --no-wait
done
Warning

Run deletion scripts with —yes only after a review period. In production, log all deletions to a storage account for audit purposes. Deleted disks cannot be recovered unless you have a snapshot.

Common mistakes

  1. Deleting the VM but not its disk, NIC, and IP. The portal lets you select associated resources during VM deletion — always check those boxes. Otherwise the orphaned components continue billing indefinitely.
  2. Stopping a VM at the OS level and assuming billing stops. An OS-level shutdown still bills for compute at the full rate. Only an Azure-level deallocate stops compute charges.
  3. Deleting a snapshot without verifying no disk depends on it. If a managed disk was created from a snapshot and the snapshot is later deleted, the disk is unaffected. But if the snapshot is being used as a restore point in a backup policy, removing it breaks the backup chain.
  4. Not checking resource locks before bulk deletes. Resources with a CanNotDelete or ReadOnly lock will silently fail to delete. Always check for locks before running batch cleanup scripts.

Frequently asked questions

Do stopped VMs still incur charges in Azure?

A stopped VM (OS-level shutdown) still incurs charges for compute and for attached managed disks. Only a deallocated VM stops compute charges, but the managed disk charge continues regardless. You must delete the disk to stop paying for it entirely.

How do I find all unattached managed disks across my subscription?

Use: az disk list --query "[?diskState=='Unattached']" --output table. This lists every managed disk in the subscription that is not currently attached to a VM.

What is the safest way to clean up resources at scale?

Tag resources with a last-validated date. Resources not re-validated within 90 days are candidates for deletion. Use Azure Policy to enforce the tag on creation, and a runbook or Logic App to flag aged resources automatically.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.