Orphaned Azure resources: the disks, IPs and snapshots you still pay for
27 Jul 2026
Orphaned Azure resources: the disks, IPs and snapshots you still pay for
Deleting a virtual machine in Azure does not delete what was attached to it. The OS disk, the data disks, the public IP that was reserved for it, the last snapshot someone took "just before the change" — all of it survives the VM and keeps billing. These orphans are the most reliable form of cloud waste there is: no user to upset, no risk to reclaiming, and yet almost every subscription carries a layer of them, because nothing in the portal shows you a resource whose parent is gone.
The usual suspects
Orphans cluster in a handful of resource types. These four cover the vast majority of what you'll find, and each has a precise signal that says "nobody owns me anymore":
| Resource | The orphan signal | Why it lingers |
|---|---|---|
| Managed disks | diskState = Unattached and no managedBy. |
VM deleted with "keep disks", or a data disk detached and never removed. |
| Public IPs | No ipConfiguration — reserved but not associated. |
Standard SKU IPs bill even when idle; the NIC or load balancer is gone. |
| Snapshots | Old timeCreated, source disk long gone. |
Taken "before a change", never labelled, never expired. They just pile up. |
| NICs & NSGs | NIC with no VM; NSG/route table with no subnet or NIC attached. | Mostly free, but they clutter the inventory and hide the ones that cost. |
Where the data lives: managedBy and ipConfiguration
You don't need a usage-metrics pipeline to find orphans — the association is
right there on the resource. Azure Resource Graph can sweep every subscription
in one query, and the tell-tale property is different per type but always
present. A disk that belongs to a VM has a managedBy pointing at
that VM's resource ID; an orphaned disk has managedBy = null and
properties.diskState = 'Unattached'. A live public IP has an
ipConfiguration; an orphaned one has none. The query below is the
whole audit for disks:
Resources
| where type == 'microsoft.compute/disks'
| where isnull(managedBy) and properties.diskState == 'Unattached'
| project name, resourceGroup, sku.name, properties.diskSizeGB, location
Three things worth knowing before you act on the output:
- Unattached is not always dead. A disk detached during a
planned migration is legitimately unattached for a week. Filter on
timeCreatedor a "last detached" age, not just the state, so you reclaim the forgotten ones and leave the in-flight ones alone. - Snapshots are cheaper than disks but never expire themselves. They bill on used size, not provisioned, so one snapshot is small — but Azure has no TTL on them, so the count only grows. Age is the right axis here.
- Basic-SKU public IPs are free; Standard-SKU are not. The migration to Standard made idle IPs a real line item. Check the SKU before you assume an unassociated IP is harmless.
Triage: three buckets, three actions
Delete now. Unattached disks and unassociated IPs older than your grace window, and snapshots whose source disk no longer exists. Nobody is using them and nothing is at risk. Snapshot the disk first if you're nervous about the data — a snapshot costs a fraction of a live disk — then delete the disk. This bucket is pure savings and the safest cleanup in Azure.
Confirm with the owner. Resources that are detached but recent, or that carry a tag pointing at a team or project. A one-line message to the owning team ("this disk has been unattached for two weeks — safe to delete?") turns a guess into a decision. Tag hygiene pays off exactly here: untagged orphans are the ones nobody can vouch for.
Keep — but read the bill anyway. Attached, in-use resources are out of scope for this audit, with one asterisk: being used doesn't mean being used efficiently. An oversized Premium disk on a VM that needs Standard is a different kind of waste — the kind you find by reading spend by resource, not by attachment state. That drill-down is covered in our guide to Azure cost access.
Make it a report, not a spring-clean
Run the Resource Graph queries once and you'll reclaim a real number this month. But the process gap that created the orphans is still open, refilling the list with every VM someone tears down. The durable fix is a standing report — standalone resources × attachment state × age — reviewed monthly, the same way you'd review dormant licensed users. GraphPaaS keeps that view per tenant, refreshed automatically: unattached disks, idle public IPs and stale snapshots are surfaced next to your Azure spend, so the monthly review is reading a list, not writing KQL. For MSPs, the same sweep runs across every subscription in every tenant you manage — the orphans nobody would ever have found by clicking through 20 portals.
See the orphaned disks, IPs and snapshots still billing in your Azure subscriptions — surfaced next to what they cost, refreshed on every sync.
Find your orphaned resources →