An Azure virtual machine shut down from inside the operating system keeps its compute reserved and keeps billing in full. Only a deallocated VM stops the compute charge. Here is how to find stopped VMs and fix them.
Azure has two different states that both look like "off". A VM shut down from inside the operating system (Start › Shut down in Windows, shutdown -h now in Linux) ends up Stopped: the guest is off, but Azure keeps the hardware reserved for it, and the compute keeps billing at the full hourly rate. A VM stopped from the Azure portal, the CLI or the API ends up Stopped (deallocated): the hardware is released and compute billing stops.
The difference is one word in the portal, and it is one of the most common sources of quiet waste in Azure subscriptions. This guide shows how to find VMs in the expensive state and how to make sure they don't end up there again.
What keeps billing in each state
| Running | Stopped | Stopped (deallocated) | |
|---|---|---|---|
| Compute (the VM size) | billed | billed | not billed |
| Managed disks | billed | billed | billed |
| Static public IP | billed | billed | billed |
| Dynamic public IP | kept | kept | released |
| Temporary disk contents | kept | kept | lost |
So even a deallocated VM still costs its disks and any static IP. That is expected; the point is to avoid paying for compute nobody is using.
1. Find VMs that are stopped but not deallocated
Portal: Virtual machines, and add the Status column. Look for Stopped without "(deallocated)".
Azure CLI (current subscription):
az vm list --show-details \
--query "[?powerState=='VM stopped'].{name:name, resourceGroup:resourceGroup, size:hardwareProfile.vmSize}" \
--output table
The --show-details flag is what adds the power state; without it the command doesn't show it.
Across every subscription you can read, Azure Resource Graph answers in one query:
az graph query -q "Resources
| where type =~ 'microsoft.compute/virtualmachines'
| where properties.extended.instanceView.powerState.code =~ 'PowerState/stopped'
| project name, resourceGroup, subscriptionId, size = properties.hardwareProfile.vmSize"
(az graph comes with the resource-graph extension; the CLI offers to install it on first use.)
2. Deallocate them
Portal: open the VM and choose Stop. When the portal stops a VM, it deallocates it.
CLI:
az vm deallocate --resource-group my-rg --name my-vm
# every stopped-but-allocated VM in a resource group
az vm deallocate --ids $(az vm list --resource-group my-rg --show-details \
--query "[?powerState=='VM stopped'].id" --output tsv)
Two things to check first: if the VM uses a dynamic public IP, it will get a different address when it starts again (switch it to static if anything depends on it), and anything kept only on the temporary disk is lost.
3. Decide what should happen to each VM
Deallocating stops the compute bill, but a VM that has been off for months is usually a VM nobody needs. For each one:
- Needed again soon: leave it deallocated, and add a tag with the owner and the reason, so the next review doesn't have to ask.
- Needed occasionally: keep the disks as snapshots (
az snapshot create) and delete the VM, or keep the VM deallocated and start it on demand. - Not needed: delete the VM, then its disks, network interface and public IP. Whether deleting a VM also deletes its disks and network interface depends on the Delete with VM option chosen when it was created, so check for orphaned disks afterwards.
4. Stop it from happening again
The expensive state comes from people shutting a VM down from the inside. Three settings make the right thing the easy thing:
- Auto-shutdown for development and test VMs:
az vm auto-shutdown --resource-group my-rg --name my-vm --time 1900shuts the VM down every evening and deallocates it. - Start and stop schedules with Azure Automation or the Start/Stop VMs solution, so nobody needs to log in to stop a VM.
- A short note for the team: "Stop from the portal or
az vm deallocate, never from inside the VM" removes most of the problem by itself.
The same idea on AWS
AWS has the opposite default: a stopped EC2 instance never bills for compute, but its EBS volumes and any attached Elastic IP keep billing. Our guide on finding wasted spend in an AWS account covers those.
Cloud Health Check checks every connected Azure subscription each week for VMs that are stopped but still allocated, along with unattached disks, unused public IPs, idle gateways and a hundred other cost, security and reliability checks. You can see a sample report on demo data with no cloud access required.
Questions about your own account? Email contact@parlak.us or try Cloud Health Check on sample data.