Azure waste

Oversized App Service plans

A web hosting plan bigger than its traffic
The agent reports this
Reversible
Why it happens

Sized for a launch that never came, or a load test that finished. One step down usually costs about half.

A plan is sized for a launch that never came, or for a load test that finished. The instance count and tier stay where they were set.

Paste this into Resource Graph Explorer in the Azure portal.

resources
| where type =~ 'microsoft.web/serverfarms'
// Scope below matches CloudFinOpsPro policy DEFAULTS: exclusions.protectTag, environment.tagKeys,
// environment.allowedTagValues, and allowUntagged false so an untagged resource counts as production.
// If your policy.yaml differs, edit the three lines below or this will disagree with your own agent.
| where tolower(tostring(coalesce(tags['cfop:protect'], ''))) !in ('true', 'yes', '1')
| extend cfopEnv = tolower(trim(' ', tostring(coalesce(tags['Environment'], tags['environment'], tags['env'], ''))))
| where cfopEnv in ('dev', 'development', 'test', 'qa', 'sandbox', 'nonprod', 'non-prod', 'staging')
| where properties.numberOfSites > 0
| extend sku = tostring(sku.name)
// Free and Shared bill nothing, so oversizing them is not a thing, and the check drops
// them before anything else. The join then keeps only SKUs with a smaller sibling:
// nextSmallerSku returns nothing at the bottom of a ladder and the check drops those too.
| where sku !in~ ('F1', 'D1')
| join kind=inner (
    datatable(sku: string, proposedSku: string) [
    'B2', 'B1',
    'B3', 'B2',
    'I2mv2', 'I1mv2',
    'I2mv4', 'I1mv4',
    'I2v2', 'I1v2',
    'I2v4', 'I1v4',
    'I3mv2', 'I2mv2',
    'I3mv4', 'I2mv4',
    'I3v2', 'I2v2',
    'I3v4', 'I2v4',
    'I4mv2', 'I3mv2',
    'I4mv4', 'I3mv4',
    'I4v2', 'I3v2',
    'I4v4', 'I3v4',
    'I5mv2', 'I4mv2',
    'I5mv4', 'I4mv4',
    'I5v2', 'I4v2',
    'I5v4', 'I4v4',
    'I6v2', 'I5v2',
    'I6v4', 'I5v4',
    'P1v3', 'P0v3',
    'P1v4', 'P0v4',
    'P2', 'P1',
    'P2mv3', 'P1mv3',
    'P2mv4', 'P1mv4',
    'P2v2', 'P1v2',
    'P2v3', 'P1v3',
    'P2v4', 'P1v4',
    'P3', 'P2',
    'P3mv3', 'P2mv3',
    'P3mv4', 'P2mv4',
    'P3v2', 'P2v2',
    'P3v3', 'P2v3',
    'P3v4', 'P2v4',
    'P4mv3', 'P3mv3',
    'P4mv4', 'P3mv4',
    'P5mv3', 'P4mv3',
    'P5mv4', 'P4mv4',
    'S2', 'S1',
    'S3', 'S2'
    ]
  ) on sku
| project name, resourceGroup, location, sku, proposedSku,
          workers = toint(properties.numberOfWorkers),
          sites = toint(properties.numberOfSites)
| order by sku asc, name asc

Each row is a plan and the exact smaller SKU in the same tier the agent would propose. Free and Shared plans are excluded because they bill nothing, so oversizing them is not a thing, and an earlier version of this query returned them. The bottom of every ladder is excluded too: B1, S1, P1, P1v2, P0v3, P1mv3, P0v4, P1mv4, I1v2, I1mv2, I1v4 and I1mv4 have no smaller sibling, so the check drops them and listing them here would be a list the agent will never act on. Isolated v2 and v4 are on the ladders: a step inside an App Service Environment resizes one plan and leaves the environment alone, and the environment charge does not change, so the saving is the plan meter difference and nothing else. Isolated v1 SKUs (I1, I2, I3) are deliberately absent because App Service Environment v1 and v2 retired on 31 August 2024, and a SKU step is the wrong advice for a plan that needs migrating. Resource Graph holds no metrics, so this cannot apply the part that decides: the check additionally requires CpuPercentage below its threshold and MemoryPercentage below its own, both averaged over the window. It then checks that the workload actually FITS the smaller SKU. Every same-tier step halves the RAM, so a plan judged comfortable on its current plan can be tight on the next one down: 45% of a P2v3's 16 GB is 7.2 GB, which is 90% of a P1v3's 8 GB. The check projects the memory peak onto the target and drops the plan if it would exceed 80%. Standard, Premium v1 and Premium v2 carry no published RAM figure, so for those the fit is reported as unchecked rather than assumed.

Applies to microsoft.web/serverfarms
What the agent does about it

A plan whose sustained CPU and memory sit far below the tier it is provisioned in.

It will not fix this one

It reports this and takes no action. A check only earns the right to change something once the action behind it can re-read live state before it fires and knows the way back.