AVD Autoscale and Scaling Plans: What They Actually Save You

Almost every AVD environment I audit is paying for compute nobody is using. Not through waste in the dramatic sense, just a host pool that was sized for Tuesday at 10am and then left running through nights, weekends and the four weeks a year when half the company is on holiday. Twenty session hosts at 168 hours a week, delivering value during maybe fifty of them.
The fix is not clever. It is a scaling plan, which is a native AVD feature, costs nothing to use, and takes about twenty minutes to configure. The reason I keep finding environments without one is that autoscale has a reputation for being risky, built on stories from the era when people bolted scaling onto AVD with Azure Automation runbooks and logic apps that occasionally deallocated a host with users on it.
That era is over. Autoscale is a first-class feature now, it handles pooled and personal host pools, and it drains sessions properly instead of pulling the rug. This post covers how to configure it, and more usefully, how to work out what it will actually save you before you build it.
I walk through the wider cost picture on video, and the ten-point audit there pairs well with what follows:
The Cost Model in One Paragraph
AVD itself is free. You pay for the session host VMs, the disks attached to them, and the storage holding profiles. The VM compute is the overwhelming majority, and it is the only part scaling touches. Disks keep billing when a VM is deallocated, and profile storage bills regardless, so the ceiling on your savings is the compute line and nothing else.
That matters because it sets realistic expectations. If someone tells you autoscale will halve your AVD bill, ask what proportion of the bill is compute. On a pool with large premium disks the answer might be 60 percent, and halving your host hours saves 30 percent overall, not 50.
Work Out the Prize First
Before configuring anything, calculate what is on the table. You need two numbers: what you pay for host compute now, and how many host hours you would actually need.
Pull the current spend:
az consumption usage list \
--start-date 2026-08-01 --end-date 2026-08-31 \
--query "[?contains(instanceName, 'avd')].{name:instanceName, cost:pretaxCost, meter:meterDetails.meterName}" \
-o table
Then work out your real occupancy from the connection data rather than from what people tell you their hours are:
WVDConnections
| where TimeGenerated > ago(30d)
| where State == "Connected"
| extend Hour = datetime_part("hour", TimeGenerated),
Day = dayofweek(TimeGenerated)
| summarize Sessions = dcount(CorrelationId) by Hour, Day
| order by Day asc, Hour asc
Almost every environment I have run this against shows the same shape. A ramp between 7 and 9, a plateau until about 16, a tail to 18, and then near zero. Weekends at under five percent of weekday peak. If your data looks like that, you are currently paying for roughly 168 hours a week to serve about 55.
Pro tip: Run this query before you promise anyone a number. I have seen a support team whose peak was 2am, and a scaling plan built on assumptions would have been an outage rather than a saving.
How Pooled Autoscale Actually Works
Scaling plans divide the day into four phases, and each phase has its own rules.
| Phase | What it does | The setting that matters |
|---|---|---|
| Ramp-up | Brings hosts online ahead of demand | Minimum hosts percent |
| Peak | Holds capacity | Load balancing algorithm |
| Ramp-down | Drains and deallocates | Force logoff behaviour |
| Off-peak | Runs the floor | Minimum hosts percent |
The mechanic that makes this safe is drain mode. When ramp-down starts, autoscale puts a host into drain mode, which stops the broker sending new sessions to it while existing sessions keep running. It waits, then either waits longer or forces sign-out depending on how you configured it.
Capacity threshold is the other concept worth understanding properly. It is the percentage of total available session capacity in use, above which autoscale starts another host. At 80 percent with ten hosts and a max of ten sessions each, autoscale adds a host when you cross 80 concurrent sessions.
Step 1: Create the Scaling Plan
RG="rg-avd-prod"
POOL="hp-avd-prod"
LOCATION="canadacentral"
az desktopvirtualization scaling-plan create \
--resource-group $RG \
--name "sp-avd-weekday" \
--location $LOCATION \
--time-zone "Pacific Standard Time" \
--host-pool-type Pooled \
--schedules '[
{
"name": "weekdays",
"daysOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"rampUpStartTime": {"hour": 7, "minute": 0},
"rampUpLoadBalancingAlgorithm": "BreadthFirst",
"rampUpMinimumHostsPct": 20,
"rampUpCapacityThresholdPct": 60,
"peakStartTime": {"hour": 9, "minute": 0},
"peakLoadBalancingAlgorithm": "DepthFirst",
"rampDownStartTime": {"hour": 17, "minute": 0},
"rampDownLoadBalancingAlgorithm": "DepthFirst",
"rampDownMinimumHostsPct": 10,
"rampDownCapacityThresholdPct": 90,
"rampDownForceLogoffUsers": false,
"rampDownWaitTimeMinutes": 30,
"rampDownNotificationMessage": "This session host is being prepared for maintenance. Please save your work and sign out.",
"offPeakStartTime": {"hour": 19, "minute": 0},
"offPeakLoadBalancingAlgorithm": "DepthFirst"
}
]'
Then assign it to the host pool, which is a separate operation and the step people forget:
az desktopvirtualization scaling-plan update \
--resource-group $RG \
--name "sp-avd-weekday" \
--host-pool-references '[{
"hostPoolArmPath": "/subscriptions/'$SUB_ID'/resourceGroups/'$RG'/providers/Microsoft.DesktopVirtualization/hostPools/'$POOL'",
"scalingPlanEnabled": true
}]'
A scaling plan that exists but is not referenced by a host pool does absolutely nothing, and the portal gives you no warning about it.
The Settings That Decide Whether This Goes Well
Load balancing algorithm, and why it flips. Breadth-first spreads users across hosts, which gives the best experience under load. Depth-first fills one host before starting the next, which is what lets you deallocate hosts at all. You want breadth during ramp-up and peak for performance, and depth during ramp-down and off-peak so sessions consolidate onto fewer machines.
Getting this backwards is the most common misconfiguration I see. Breadth-first during ramp-down means your users are spread thinly across every host and autoscale cannot deallocate any of them.
Force logoff, and why I leave it off. rampDownForceLogoffUsers: false means autoscale drains gracefully and will not evict anyone. The cost is that a single user who leaves a session connected overnight keeps a host alive.
I still leave it off for the first month. Turn it on once you know your usage pattern, and pair it with a sign-out policy so people are not surprised.
Warning: Force logoff does exactly what it says. Combined with a short wait time it will close applications with unsaved work. If you enable it, set the wait to at least 30 minutes and make sure the notification message is one your users will actually read.
Minimum hosts percent during off-peak. Setting this to zero saves the most and means the first person to sign in at 6am waits for a VM to boot and register, which is several minutes. Ten percent keeps one host of ten warm. On a pool serving a single time zone I use zero and pair it with Start VM on Connect. Across time zones, keep a floor.
Personal Host Pools Are Different
Personal desktop autoscale is generally available and it works on a different principle. There is no capacity threshold, because each user has their own machine. Instead, actions are driven by user session state.
az desktopvirtualization scaling-plan create \
--resource-group $RG \
--name "sp-avd-personal" \
--location $LOCATION \
--time-zone "Pacific Standard Time" \
--host-pool-type Personal \
--schedules '[
{
"name": "weekdays-personal",
"daysOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"rampUpStartTime": {"hour": 7, "minute": 0},
"rampUpAutoStartHosts": "WithAssignedUser",
"rampUpActionOnDisconnect": "None",
"rampUpActionOnLogoff": "None",
"peakStartTime": {"hour": 9, "minute": 0},
"peakActionOnDisconnect": "None",
"peakActionOnLogoff": "None",
"rampDownStartTime": {"hour": 18, "minute": 0},
"rampDownActionOnDisconnect": "Deallocate",
"rampDownDisconnectDelayMinutes": 60,
"rampDownActionOnLogoff": "Deallocate",
"offPeakStartTime": {"hour": 20, "minute": 0},
"offPeakActionOnDisconnect": "Deallocate",
"offPeakDisconnectDelayMinutes": 30,
"offPeakActionOnLogoff": "Deallocate"
}
]'
The savings on personal pools are usually larger in percentage terms, because a personal desktop is idle far more of the time than a pooled host. The tradeoff is start time. A user coming back at 8pm waits for their machine to resume.
Hibernation improves that considerably, because session state persists rather than the machine cold booting. It is still in preview, so treat it as something to pilot rather than something to standardise on.
What This Is Worth
Take a realistic pool: ten D4as v5 session hosts, running 24/7.
Those run roughly 730 hours a month each, so 7,300 host hours. With a scaling plan covering weekday business hours with a small off-peak floor, you land nearer 2,400 host hours. That is a two-thirds reduction in compute, and compute is the part of the AVD bill that responds to any of this.
Two things trim that in practice. Disks keep billing while deallocated, so a pool with large premium OS disks sees a smaller percentage saving. And a graceful ramp-down means stragglers keep hosts alive, which is worth a few percent.
I tell clients to expect a 40 to 55 percent reduction on the total AVD line for a standard weekday pattern. Anyone promising more is quoting the compute saving and ignoring the rest of the bill.
What Will Bite You
Nothing scaled and there are no errors. The plan is not assigned to the host pool, or scalingPlanEnabled is false on the reference. This is the most common one by a distance.
Hosts scaled down during business hours. Your time zone on the scaling plan does not match reality. The plan uses its own configured zone, not the VM's and not yours.
Autoscale will not deallocate anything despite low usage. Load balancing is breadth-first during ramp-down, so sessions are spread across every host and none can be emptied. Switch ramp-down and off-peak to depth-first.
A host sits in drain mode indefinitely. Someone has a disconnected session on it and force logoff is off. That is the design working as configured, not a fault. Add a sign-out policy for disconnected sessions.
Costs did not move much. Check your disk spend. A pool of ten hosts with 512 GB premium SSDs is carrying a meaningful monthly cost that scaling cannot touch, and the answer there is right-sizing the disks rather than the schedule.
Gotcha: Autoscale will not deallocate a host that has a session on it in any state, including disconnected, unless you enable force logoff. A user who closes their laptop lid on Friday afternoon without signing out can keep a session host running all weekend.
Verify It Is Working
Do not wait for the invoice. Watch the host states the day after you enable it.
AzureActivity
| where TimeGenerated > ago(7d)
| where OperationNameValue has "Microsoft.Compute/virtualMachines/deallocate"
or OperationNameValue has "Microsoft.Compute/virtualMachines/start"
| where Caller has "WindowsVirtualDesktop"
| summarize Actions = count() by bin(TimeGenerated, 1h),
Operation = tostring(split(OperationNameValue, "/")[-1])
| order by TimeGenerated asc
You should see a clean pattern of starts in the morning and deallocates in the evening. Starts and deallocates within the same hour means your capacity threshold is too tight and autoscale is flapping, which costs more than doing nothing because you pay the boot time repeatedly.
Conclusions
Scaling plans are the highest-return change available in most AVD environments and they take an afternoon. Run the occupancy query first so you know the shape of your actual demand, set breadth-first for ramp-up and depth-first for ramp-down, leave force logoff off until you trust the pattern, and assign the plan to the host pool.
Then check the disk line on your bill, because once compute is scheduled properly, disks are usually the next largest thing nobody has looked at.
What's Next
- Azure Monitor for AVD to confirm scaling is not degrading sign-in times during ramp-up
- Right-sizing session host disks, which is where the remaining fixed cost sits
- FSLogix on Azure Files, because consolidating sessions onto fewer hosts during ramp-down puts more load on the profile share at exactly the wrong moment




