What you usually find
- 40 plans, many at 5–10% CPU, each with 2–3 instances.
- Old SKUs (Standard, Premium v2) when newer generations give more for the money.
- Instance counts set during a launch and never changed.
You pay per plan instance, not per app, so this is where the savings are.
1. Inventory
az appservice plan list --query "[].{plan:name, rg:resourceGroup, sku:sku.name, workers:sku.capacity, apps:numberOfSites, os:kind}" -o table
// Azure Resource Graph: apps per plan
resources
| where type == "microsoft.web/sites"
| extend plan = tostring(split(properties.serverFarmId, "/")[-1])
| summarize apps = make_list(name) by plan, resourceGroup
2. Measure per plan
From plan metrics over 30 days: CpuPercentage and MemoryPercentage (p95 and max), plus per-app Requests, Http5xx, AverageResponseTime.
| Plan profile | Action |
|---|---|
| p95 CPU < 20%, memory < 50% | Fewer instances, smaller SKU, or merge with another plan |
| Memory high, CPU low | Memory-optimised SKU (P*mv3) or fewer apps per plan |
| Spiky CPU | Autoscale instead of a high fixed count |
| Consistently busy | Leave it; maybe a newer SKU |
3. Consolidate compatible apps
Apps can share a plan when they have the same OS, region and resource group/webspace, similar compliance needs, and no "noisy neighbour" risk to each other. Typical merges: several small internal apps; an app with its admin portal.
Move an app to another plan (same resource group and region):
az webapp update -g rg-web -n intranet-api --set serverFarmId=/subscriptions/<sub>/resourceGroups/rg-web/providers/Microsoft.Web/serverfarms/plan-shared-p1v3
Don't merge production with dev/test, and keep critical customer-facing apps on their own plans.
4. Per-app scaling for dense plans
On a shared plan, cap busy apps so they don't use every instance:
az appservice plan update -g rg-web -n plan-shared-p1v3 --per-site-scaling true
az webapp update -g rg-web -n intranet-api --set siteConfig.numberOfWorkers=2
5. Move to current SKUs
Changing SKU within the same family is an in-place scale. Newer Premium generations often give better price per core and memory. Test in a slot or non-prod first; a move to a different deployment unit may change outbound IPs, so check firewall allow-lists that use them.
az appservice plan update -g rg-web -n plan-orders --sku P1v3
6. Autoscale
Rule-based (Standard and up):
az monitor autoscale create -g rg-web --resource plan-orders --resource-type Microsoft.Web/serverfarms \
--name as-plan-orders --min-count 2 --max-count 8 --count 2
az monitor autoscale rule create -g rg-web --autoscale-name as-plan-orders \
--condition "CpuPercentage > 70 avg 10m" --scale out 2
az monitor autoscale rule create -g rg-web --autoscale-name as-plan-orders \
--condition "CpuPercentage < 35 avg 15m" --scale in 1
Keep a gap between scale-out and scale-in thresholds to avoid flapping, and add a schedule profile if load is predictable (office hours).
Automatic scaling (Premium v2/v3 and newer): the platform scales on HTTP load, with pre-warmed instances:
az appservice plan update -g rg-web -n plan-orders --elastic-scale true --max-elastic-worker-count 10
az webapp update -g rg-web -n orders-api --minimum-elastic-instance-count 2 --prewarmed-instance-count 1
7. Keep at least two instances in production
Two instances (ideally zone-redundant plans) keep the app up during platform maintenance and instance restarts. Savings shouldn't come from going down to one.
Tip
Turn on Health check on every app before tuning autoscale. Without it, unhealthy instances stay in rotation and scaling decisions are based on a broken instance's metrics.
Checklist
- Plans, apps and SKUs inventoried
- 30-day CPU/memory p95 per plan
- Compatible apps consolidated; prod separated from non-prod
- Per-app scaling on dense plans
- Current SKUs; outbound IP changes checked
- Autoscale or automatic scaling with ≥ 2 instances in prod