Skip to main content
emnode
Cost

Delete empty App Service plans

An App Service plan is billed for the compute it reserves, not for the apps you run on it. A plan with no apps on a paid tier is pure waste: it bills every hour and serves nothing. This lesson finds those empty plans and removes them safely.

12 min·10 sections·AZURE

Last reviewed

Empty App Service plans: the basics

Why does a plan with no apps still cost money?

An App Service plan ('Microsoft.Web/serverfarms') is the compute behind your web apps, API apps and function apps. The crucial thing to understand is the billing model: on every paid tier (Basic, Standard, PremiumV2, PremiumV3 and the Isolated tiers), you pay for the plan, not for the apps. The plan reserves a number of VM instances at a given size, and you are charged for those instances by the hour for as long as the plan exists, whether one app runs on it, ten apps run on it, or no app runs on it at all.

That makes the empty plan a uniquely pure form of waste. When the last app is deleted or moved, the plan does not stop billing. It keeps reserving its instances and keeps charging the per-hour rate, but nothing is using them. There is no traffic to serve, no value being delivered, and yet the meter runs exactly as it did before. Azure Advisor surfaces this as a cost recommendation against 'microsoft.web/serverfarms': the plan has no apps running, so delete it to save the spend.

Most empty plans are leftovers, not decisions. A web app was retired but the plan it sat on was left behind. A workload was migrated to a different plan or to Container Apps and the old plan was forgotten. A proof of concept was torn down app-first and the plan never followed. The Free (F1) and Shared (D1) tiers are the one exception worth remembering: they genuinely cost nothing, so an empty F1 plan is harmless. Every empty plan on a paid tier, though, is money leaving the account for nothing in return.

In this lesson you will learn how App Service plan billing actually works, how to find every empty plan on a paid tier across a subscription reliably, why the obvious CLI shortcut can mislead you, and how to delete an empty plan safely without orphaning a hidden app, slot or function. The keep-learning section links the exact Microsoft references for billing, plan management and the Advisor recommendation behind this work.

Fun fact

The plan that outlived its app by a year

The most expensive empty App Service plans are almost never the ones anyone notices. They are the single Standard or Premium plans left behind when a web app was retired, sitting in a resource group nobody opens, billing their per-hour rate month after month for compute that serves no request. Because the charge is modest on any single line and constant rather than spiking, it never trips a budget alert and never stands out in a cost review. The plan can comfortably outlive the app it was created for by a year or more, quietly charging the whole time, until someone runs the one query that asks: which of these plans actually has an app on it?

Finding empty App Service plans across a subscription

Priya runs the platform team at a company doing a half-year cost review. Azure Advisor has flagged several App Service plans with no apps under its Cost recommendations, and she wants the full picture before deleting anything.

She knows a trap is waiting: the obvious 'az appservice plan list' command has a long-standing quirk where it reports numberOfSites as 0 for every plan, so it cannot be trusted to tell empty from busy. She uses the per-plan show command, which reports the real count, to separate the genuinely empty paid plans from the ones still doing work.

List the plans, then ask each one for its real app count with 'az appservice plan show'. The per-plan numberOfSites is the trustworthy signal.

$ az appservice plan list --query "[].{name:name, rg:resourceGroup}" -o tsv | while read name rg; do count=$(az appservice plan show -n "$name" -g "$rg" --query numberOfSites -o tsv); tier=$(az appservice plan show -n "$name" -g "$rg" --query sku.tier -o tsv); echo "$name $tier sites=$count"; done
plan-web-prod Standard sites=3
plan-api-legacy Standard sites=0
plan-poc-2024 PremiumV3 sites=0
plan-sandbox Free sites=0
# plan-api-legacy and plan-poc-2024 are empty AND paid: delete candidates.
# plan-sandbox is empty but Free, so it costs nothing: leave it.

An empty plan on a paid tier (Standard, PremiumV3 here) is a delete candidate. An empty Free plan costs nothing, so it is not worth the change. numberOfSites comes from 'show', not 'list', because 'list' reports 0 for everything.

How Azure decides a plan is empty, and why the count can mislead youdeep dive

The signal for an empty plan is the 'numberOfSites' property on the serverfarm resource. It counts everything hosted on the plan: web apps, API apps, function apps on a dedicated plan, and crucially their deployment slots, since each slot is itself a site. When numberOfSites is 0, nothing at all is hosted, which is exactly the condition Azure Advisor uses to raise its 'no apps running, consider deleting' cost recommendation against the plan.

There is a well-known trap. The 'az appservice plan list' command has reported numberOfSites as 0 for every plan regardless of reality for a long time, so filtering the list output by that field would flag busy plans as empty and risk deleting live compute. The 'az appservice plan show' command, queried per plan, returns the true count. The same true count is available estate-wide and quickly through Azure Resource Graph, which reads the real property: that is the reliable way to sweep many subscriptions at once.

The other thing to know is what numberOfSites does not always make obvious at a glance: a count of 0 includes the case where the only things left are deployment slots or a function app that the portal's Apps tab can present confusingly. That is why the safe deletion path verifies with 'az webapp list' and 'az functionapp list' filtered by the plan's serverFarmId before removing it, rather than trusting a single number. A plan that still has any site attached cannot be deleted until that site is removed, so the verification both protects you and explains the occasional 'cannot delete' error.

What is the impact of leaving empty plans running?

The direct impact is the simplest kind of waste: a recurring charge for compute that does no work. Because paid App Service plans bill per reserved VM instance rather than per app, an empty plan charges its full hourly rate continuously. Nothing about it being empty reduces the bill; only deleting it or dropping it to the Free tier does. Every month it survives is a month of paying for capacity that serves no request.

The second-order impact is that empty plans hide. The charge is steady and modest on any single line, so it never spikes a budget alert and rarely stands out in a cost review next to the genuinely large items. A plan can outlive its app by a year, billing the whole time, simply because nothing draws attention to it. The leak persists not because it is large but because it is quiet and unowned.

There is also an estate-hygiene cost. Stale plans clutter inventory, blur the picture of what compute is actually in use, and make capacity and cost reporting less trustworthy. A subscription where every paid plan provably hosts an app is one where the App Service line on the bill maps cleanly to running workloads, which is the state you want before any deeper rightsizing or commitment-discount work.

How do you clear empty plans safely?

Work it as one loop: find the empty paid plans reliably, confirm each is truly empty before deleting, then make the check recurring so they do not pile up again. The order matters, because the only real risk is deleting a plan that still has a hidden site on it.

1. Inventory plans by real app count and tier

Get the true numberOfSites per plan from 'az appservice plan show' or Azure Resource Graph, never from 'az appservice plan list', which reports 0 for everything. Pair each empty plan with its sku.tier and instance count so you know which are paid (delete candidates) and which are Free or Shared (leave them, they cost nothing).

2. Confirm each candidate is genuinely empty

Before deleting, verify nothing is hidden on the plan: list any web apps and function apps whose serverFarmId points at it, including their deployment slots, which each count as a site. If anything is still attached, the plan is not empty, and Azure will refuse the delete until that site is removed. This step is what makes the deletion risk-free.

3. Delete the empty paid plans, highest run rate first

Order by monthly cost so the Premium and multi-instance plans go first. Delete each confirmed-empty plan. The charge stops immediately on deletion. If a plan is being deliberately kept warm for an imminent app, record that as a named exception with the carrying cost noted, rather than leaving it as undocumented waste.

4. Make the check recurring so they do not return

Empty plans are a flow, not a one-off: new ones appear every time an app is retired. Put the Resource Graph query on a monthly cadence, or wire it into a cost dashboard, so an empty paid plan is caught within weeks rather than discovered a year later. This turns a periodic cleanup into a closed leak.

# 1. Find empty plans estate-wide with the RELIABLE count (Resource Graph,
#    not 'az appservice plan list', which reports numberOfSites as 0 for all).
az graph query -q "resources \
  | where type =~ 'microsoft.web/serverfarms' \
  | where properties.numberOfSites == 0 \
  | where sku.tier !in ('Free','Shared') \
  | project name, resourceGroup, tier=sku.tier, capacity=sku.capacity" \
  -o table

# 2. For each candidate, CONFIRM nothing is hidden on it before deleting.
PLAN=plan-api-legacy
RG=rg-legacy
planId=$(az appservice plan show -n "$PLAN" -g "$RG" --query id -o tsv)
az webapp list      -g "$RG" --query "[?serverFarmId=='$planId'].name" -o tsv
az functionapp list -g "$RG" --query "[?serverFarmId=='$planId'].name" -o tsv
# Both must return nothing. If either lists a name, remove that app/slot first.

# 3. Delete the confirmed-empty plan. Billing stops on deletion.
az appservice plan delete --name "$PLAN" --resource-group "$RG" --yes

Quick quiz

Question 1 of 5

Azure Advisor flags an App Service plan as 'no apps running, consider deleting'. Why does an empty plan still cost money?

You can now treat empty App Service plans as a clean, closable form of waste: get the real app count from 'az appservice plan show' or Resource Graph rather than the misleading 'list' output, separate the empty paid plans from the harmless Free ones, confirm nothing is hidden on a plan before deleting it, and put the check on a monthly cadence so the leak stays closed. Azure Advisor will keep pointing at the symptom; you now know how to bank the saving for good.

Back to the library