Skip to main content
  1. Posts/

Windows 365 Disk Utilization Reporting

·3119 words·15 mins
Bradley Wyatt
Author
Bradley Wyatt
Sr. Solutions Engineer @ Microsoft
Table of Contents

Overview
#

The Windows 365 Cloud PC Monitors feature is currently in Public Preview and already provides a broad range of valuable insights into your Cloud Endpoints. This includes sign-in time trends, Cloud PC health trends, average round-trip times, connection data by Windows App version and a much more. However, at the time of writing, the platform does not provide reporting for Cloud PC disk space trends, particularly around free and used disk space. Luckily, we can still get this information using the Microsoft Graph REST API.

Microsoft Graph managedDevices
#

Because every Cloud PC is also an Intune-enrolled Windows device, the Microsoft Intune device-inventory pipeline carries OS disk values for each one. Graph exposes these via the managedDevice resource — specifically two read-only Int64 properties inherited by windowsManagedDevice:

PropertyTypeMeaning
totalStorageSpaceInBytesInt64Total OS volume capacity
freeStorageSpaceInBytesInt64Free space on the OS volume

Both come straight off the device at Intune check-in time and apply to the system drive (typically C:). They are documented on the shared managedDevice resource type and are inherited by windowsManagedDevice. The cloudPC resource itself does not carry these properties — you must join across.

Filtering managedDevices to the Cloud PC fleet
#

Cloud PCs are not flagged with a dedicated managedDeviceOwnerType value; they appear as ordinary company-owned Windows devices. Currently, we can distriguish Cloud PCs using the following methods:

  1. Managed Device model property contains Cloud PC: Every W365 SKU’s model string starts with “Cloud PC " (e.g. Cloud PC Enterprise 2vCPU/4GB/64GB). This is the simplest one-shot filter and is what the Intune admin center itself uses.

    The Microsoft Graph deviceManagement/managedDevices endpoint returns all managed devices and properties for total storage (totalStorageSpaceInBytes) and free space (freeStorageSpaceInBytes). It also include our model property that we can filter against.

    In my example, using the Microsoft Graph PowerShell SDK, I can run, Get-MgDeviceManagementManagedDevice -All -Property * | Where-Object { $_.Model -like 'Cloud PC*'} | Select-Object Id, Total*, Free*, Model in my tenant which displays my current Cloud PC’s and all the properties I will need:

    Id                                   TotalStorageSpaceInBytes FreeStorageSpaceInBytes Model
    --                                   ------------------------ ----------------------- -----
    3c787e74-e2d2-4fce-9cd1-650b5e06a4ee             136844410880             95460261888 Cloud PC Frontline 4vCPU/16GB/12…
    22bb1414-aa07-4d5d-8d5d-a7af104d1dae             136844410880             99693363200 Cloud PC Frontline Shared 4vCPU/…
    cc802f52-f11e-43df-8393-565439c1a7c6             274283364352            222822400000 Cloud PC Enterprise 8vCPU/32GB/2…
  2. Match Cloud PCs from /deviceManagement/virtualEndpoint/cloudPCs to Get-MgDeviceManagementManagedDevice: Another way to get Cloud PCs is to use the /deviceManagement/virtualEndpoint/cloudPCs Graph endpoint which will give us a list of Cloud PCs like below:

{
    "id": "95194d88-cec5-4b65-af62-26dbd1814364",
    "displayName": "W365-Frontline-Dedicated - Bradley Wyatt",
    "imageDisplayName": "Windows 11 Enterprise + Microsoft 365 Apps 25H2",
    "provisioningPolicyId": "27d60840-6888-44b9-aefd-e5f8f1a92add",
    "provisioningPolicyName": "W365-Frontline-Dedicated",
    "onPremisesConnectionName": "",
    "servicePlanId": "dd3801e2-4aa1-4b16-a44b-243e55497584",
    "servicePlanName": "Cloud PC Frontline 4vCPU/16GB/128GB",
    "userPrincipalName": "brad@windowsfromanywhere.com",
    "lastModifiedDateTime": "2026-05-15T00:01:02Z",
    "managedDeviceId": "3c787e74-e2d2-4fce-9cd1-650b5e06a4ee",
    "managedDeviceName": "CFD-brad-HNFI4",
    "aadDeviceId": "db5913da-e353-49d7-9570-0f2d7f13b51a",
    "gracePeriodEndDateTime": null,
    "provisioningType": "shared"
}

But you may have noticed that this endpoint does not include any storage information, therefore you will need to iterate through each item and match the managedDeviceId value to Id from the deviceManagement/managedDevices endpoint.

PowerShell example using Microsoft.Graph SDK
#

The following PowerShell script lists every Cloud PC in the tenant with its free / total storage in GB and a percent-free figure. It uses the Microsoft Graph PowerShell SDK and the v1.0 endpoint.

# Requires: Install-Module Microsoft.Graph -Scope CurrentUser
Connect-MgGraph -Scopes 'DeviceManagementManagedDevices.Read.All'

# Pull every managedDevice whose model starts with "Cloud PC"
$cpcs = Get-MgDeviceManagementManagedDevice -All -Property * |  Where-Object { $_.Model -like 'Cloud PC*' }

$cpcs | ForEach-Object {
    $totalGB = [math]::Round($_.TotalStorageSpaceInBytes / 1GB, 2)
    $freeGB  = [math]::Round($_.FreeStorageSpaceInBytes  / 1GB, 2)
    $pctFree = if ($totalGB -gt 0) { [math]::Round(($freeGB / $totalGB) * 100, 1) } else { 0 }

    [pscustomobject]@{
        DeviceName   = $_.DeviceName
        UPN          = $_.UserPrincipalName
        Model        = $_.Model
        TotalGB      = $totalGB
        FreeGB       = $freeGB
        PctFree      = $pctFree
        LastSyncUtc  = $_.LastSyncDateTime
    }
} | Sort-Object PctFree | Format-Table -AutoSize

Results would look like the following:

DeviceName      UPN                          Model                                      TotalGB FreeGB PctFree LastSyncUtc
----------      ---                          -----                                      ------- ------ ------- -----------
CFD-brad-HNFI4  brad@windowsfromanywhere.com Cloud PC Frontline 4vCPU/16GB/128GB         127.45  88.90   69.80 5/14/2026 7:58:34 PM
CFS-Q4L0XGCNSTR                              Cloud PC Frontline Shared 4vCPU/16GB/128GB  127.45  92.85   72.90 5/15/2026 4:42:12 AM
CPC-brad-VO992  brad@windowsfromanywhere.com Cloud PC Enterprise 8vCPU/32GB/256GB        255.45 207.52   81.20 5/14/2026 8:01:37 PM
Note

Flex Shared with User experience sync enabled devices will not yield accurate results since the user profiles are wiped and then loaded on at login.

The lastSyncDateTime column is critical: a Cloud PC that hasn’t checked in for a week will show stale storage values. Filter or flag rows whose last sync is older than the customer’s reporting tolerance.

Required delegated or application permission: DeviceManagementManagedDevices.Read.All.

How fresh is the data?
#

PathFreshnessNotes
Graph managedDevicesLast Intune check-in (~8 hr default for Windows)Force a Sync to refresh; honour lastSyncDateTime

Per-user disk projection on Windows 365 Frontline / Flex Shared Cloud PCs
#

Important

The Graph managedDevices approach above answers “how full is this Cloud PC right now?” It does not answer “how big is user X’s footprint when they sign in to a shared Cloud PC?” On Windows 365 Flex Shared (the SKU formerly known as Frontline shared), the OS volume is a moving target: per windows 365 flex snapshot based reset the disk reverts to a known-good snapshot at sign-out, so any free-space reading is bound to whichever user is currently signed in and is wiped before the next user starts. Per-user visibility on a shared Cloud PC requires a different pipeline.

Conceptual model — what a “per-user” disk reading means here
#

A user’s effective on-disk consumption when they sign in to a Flex Shared Cloud PC is the sum of three things, none of which freeStorageSpaceInBytes measures on its own:

  1. The base OS image footprint — fixed per provisioning policy. Whatever the gallery image plus admin-installed apps consume on a freshly snapshot-reset OS volume. This is the floor for every user on every Cloud PC in the policy. A 128 GB OS disk that boots to the snapshot at 52 GB used has 76 GB headroom before the user logs in.
  2. The per-user persistence footprint — what [[cloud-pc-flex-user-experience-sync|User Experience Sync]] (UES) re-attaches at sign-in, drawn from Microsoft-managed pooled storage. This is the only longitudinal per-user value the platform exposes natively. When attached, its contents are projected into C:\Users\<upn> and count against the OS volume.
  3. Session-time growth — temp files, browser cache, OneDrive hydration of Files On-Demand placeholders, app caches, downloads. This is volatile and disappears on the next snapshot reset, but determines the peak disk pressure during a session.

Anything redirected off-box — KFM-redirected Desktop / Documents / Pictures pointing at the user’s OneDrive, Edge sync data, Outlook OST stored in User Experience Sync (UES) (or excluded entirely) — does not consume CPC OS-volume capacity at sign-in, but it does drive hydration traffic and may briefly land on disk if the user opens those files. UES explicitly excludes AppData\Local\Packages\*\AC, SystemAppData, LocalCache, TempState, the identity broker caches, and other non-roamable data, so the persistence container is a settings + user files footprint, not a full profile clone.

Warning

[[cloud-pc-flex-user-experience-sync|UES]] is not [[fslogix-profile-containers|FSLogix]]. The W365 Flex Shared profile model is Microsoft-managed pooled storage attached by the platform; there is no SMB share, no %username%_%sid%.vhdx to enumerate, and no FSLogix container to mount. Direct migration from FSLogix to UES is explicitly not supported. FSLogix remains the [[azure-virtual-desktop|AVD]] multi-session pattern — relevant for parity conversations and for customers who run BYOI configurations, but not for stock W365 Flex Shared. The “enumerate VHDX files in the profile share” approach customers ask about is an AVD/FSLogix recipe; on W365 the equivalent surface is the UES Graph endpoints below.

Approach 1 — UES per-user storage via Microsoft Graph (canonical)
#

The Intune admin center exposes UES per-user storage at Devices → Provision Cloud PCs → Provisioning policies → (policy) → User Storage tab. The same data is available programmatically through two beta Graph endpoints under the policy assignment:

EndpointReturns
…/cloudPCUserSettingsPersistence/retrieveUserSettingsPersistenceProfileUsage(configurationId='{id}')Pool aggregates: totalAllocatedStorageInGB, usedStorageInGB, remainingAvailableStorageInGB.
…/cloudPCUserSettingsPersistence/retrieveUserSettingsPersistenceProfiles(configurationId='{id}')Per-user profiles: userPrincipalName, profileSizeInGB, lastProfileAttachedDateTime, status (connected / notConnected / deleting).

profileSizeInGB is the maximum allocated size for the user (the tier picked at policy creation: 4 / 8 / 16 / 32 / 64 GB), not the in-use bytes. The platform does not currently surface a per-user used figure on the persistence profile object — the only “used” value is the policy-pool aggregate. That is a real gap for capacity planning at the user level; the documented workaround is to inspect C:\Users\<upn> size from inside an active session (Approach 4 below) and treat the persistence-tier value as the ceiling.

Both endpoints require CloudPC.Read.All and live under /beta. The gracePeriodEndDateTime on cloudPCUserSettingsPersistenceDetail is the cut-off after which the platform auto-evicts the oldest-last-attached profile when the pool is over its limit — useful as a “who’s about to be auto-cleaned?” signal.

# Requires: Install-Module Microsoft.Graph.Beta -Scope CurrentUser
Connect-MgGraph -Scopes 'CloudPC.Read.All'

$policyId     = '<provisioning-policy-id>'
$assignmentId = '<assignment-id>'
$configId     = '<persistence-configuration-id>'   # from cloudPCUserSettingsPersistenceDetail

$profilesUri = "https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/" +
               "provisioningPolicies/$policyId/assignments/$assignmentId/" +
               "cloudPCUserSettingsPersistence/retrieveUserSettingsPersistenceProfiles" +
               "(configurationId='$configId')"

$profiles = (Invoke-MgGraphRequest -Method GET -Uri $profilesUri).value

$profiles | Select-Object userPrincipalName,
                          profileSizeInGB,
                          status,
                          @{n='LastAttached';e={$_.lastProfileAttachedDateTime}} |
            Sort-Object LastAttached -Descending |
            Export-Csv .\flex-shared-ues-profiles.csv -NoTypeInformation

Pair this with the alert rule Windows 365 Flex Cloud PC User Experience Sync Storage Limits under Tenant administration → Cloud PC Alerts so admins are notified before the pool hits its tolerance window.

Approach 2 — OneDrive quota per user via Graph
#

Because Flex Shared deployments overwhelmingly pair UES with OneDrive Known Folder Move (UES does not retain Packages\*\LocalCache-style caches and the customer guidance is to push Desktop / Documents / Pictures to OneDrive), a meaningful chunk of “user data” lives off-CPC. Two Graph paths surface it per user:

# Per-user, real-time
Get-MgUserDrive -UserId user@contoso.com |
    Select-Object @{n='UPN';e={'user@contoso.com'}},
                  @{n='UsedGB';e={[math]::Round($_.Quota.Used/1GB,2)}},
                  @{n='TotalGB';e={[math]::Round($_.Quota.Total/1GB,2)}}
# Tenant-wide CSV (D7 / D30 / D90 / D180 periods)
GET https://graph.microsoft.com/v1.0/reports/getOneDriveUsageAccountDetail(period='D30')

The CSV returns Owner Principal Name, Storage Used (Byte), Storage Allocated (Byte), File Count, and Last Activity Date — joinable on UPN against the UES profile list to produce a per-user “where their data actually lives” view.

Approach 3 — Sign-in correlation via cloudPcAuditEvent + connectivity history
#

To attribute a particular OS-disk free-space reading to a specific user session, you need a timeline of who was signed in to which Cloud PC when. Two surfaces:

  • cloudPcAuditEvent (Graph v1.0) at /deviceManagement/virtualEndpoint/auditEvents — records administrative and lifecycle activities (provision, reprovision, restore, snapshot reset). Useful for marking the boundary at which the OS disk was reset to baseline.
  • [[cloud-pc-connectivity-history-report|User connectivity history report]] — Cloud PC user start/finish connection activities, surfaced in the Intune admin centre under Devices → Cloud PC performance. This is the closest the platform comes to a “who was signed in to this CPC at time T?” feed for Flex Shared.

Joining the connectivity history (user, start, end, Cloud PC ID) to Graph managedDevices.lastSyncDateTime plus the storage values lets you attribute that snapshot of freeStorageSpaceInBytes to that user’s session — provided the Intune check-in landed during their connected window.

Approach 4 — In-session telemetry (KQL)
#

For customers who ship Cloud PC Perf counters to Log Analytics via the Azure Monitor Agent, DeviceLogonEvents (Defender for Endpoint advanced hunting) joined with the LogicalDisk free-space sample at the same minute attributes the disk reading to the signed-in user:

let logons =
    DeviceLogonEvents
    | where ActionType == "LogonSuccess" and LogonType in ("Interactive","RemoteInteractive")
    | project Computer = DeviceName, AccountUpn, LogonTime = Timestamp;
let disk =
    Perf
    | where ObjectName == "LogicalDisk" and CounterName == "Free Megabytes" and InstanceName == "C:"
    | project Computer, FreeMB = CounterValue, TimeGenerated;
disk
| join kind=inner (logons) on Computer
| where TimeGenerated between (LogonTime .. LogonTime + 8h)
| summarize MinFreeMB = min(FreeMB), MaxFreeMB = max(FreeMB) by Computer, AccountUpn, LogonTime
| order by LogonTime desc

This is the only path that produces an honest “user X drove the disk down to N MB free during their session.” It requires AMA + a DCR shipping LogicalDisk(*)\Free Megabytes and Defender for Endpoint onboarding — see [[microsoft-defender-for-endpoint-on-cloud-pc|MDE on Cloud PC]] for the prereqs.

Approach 5 — In-session inspection from a remediation script
#

This is the [[intune-proactive-remediations|Intune Remediations]] path — paired detection + remediation PowerShell scripts shipped through the Intune Management Extension and harvested back via the report or Graph deviceRunStates.

A scheduled remediation script that runs in the user context on the active Cloud PC can size C:\Users\<upn> directly and write the result to the Remediations report. This is the most direct measurement of the user’s actual on-disk footprint during a session, but it requires (a) the user to be signed in when the script runs and (b) acceptance that the result is wiped at the next snapshot reset.

# Detection script — emits per-user used GB for the currently signed-in user
$upn  = (Get-CimInstance Win32_ComputerSystem).UserName
$path = "C:\Users\$($env:USERNAME)"
$used = (Get-ChildItem $path -Recurse -Force -ErrorAction SilentlyContinue |
         Measure-Object Length -Sum).Sum
"$upn,$([math]::Round($used/1GB,2))"
exit 0

Combined per-user projection model
#

To answer Bradley’s framing question — “if user X connects to this shared CPC, what does their disk footprint look like?” — combine the three independent signals into a projected on-disk consumption value:

ProjectedUsedGB(user)  ≈  BaseImageGB(policy)
                        + UES.profileSizeInGB(user)              # ceiling, not in-use
                        + ExpectedSessionGrowthGB(user)          # rolling avg from Approach 4
                        + OneDriveHydrationGB(user, files-on-demand cache)

ProjectedFreeGB(user)  =  TotalStorageSpaceInBytes(policy) / 1GB  -  ProjectedUsedGB(user)

In script form, joining Approach 1 and Approach 2 with a fixed base-image figure for the policy:

# Pseudocode — assumes $profiles from Approach 1 and a $baseImageGB constant per policy
$baseImageGB = 52   # measured once on a freshly snapshot-reset Cloud PC for this policy

$projection = foreach ($p in $profiles) {
    $od = Get-MgUserDrive -UserId $p.userPrincipalName -ErrorAction SilentlyContinue
    [pscustomobject]@{
        UPN              = $p.userPrincipalName
        UESCeilingGB     = $p.profileSizeInGB
        OneDriveUsedGB   = if ($od) { [math]::Round($od.Quota.Used/1GB,2) } else { 0 }
        LastAttached     = $p.lastProfileAttachedDateTime
        ProjectedMaxOnDiskGB = $baseImageGB + $p.profileSizeInGB
    }
}
$projection | Sort-Object ProjectedMaxOnDiskGB -Descending | Format-Table

The result is a ceiling projection — what the OS volume could show in-use if the user fills their UES allotment and pulls a chunk of their OneDrive into the local Files On-Demand cache. Combine with Approach 4’s rolling average for a realistic working figure.

Limitations specific to the Frontline / Flex Shared model
#

  • profileSizeInGB is allocation, not usage. The Graph endpoint exposes the tier, not bytes consumed inside the container. For “how full is user X’s UES storage?” the only first-party answer today is the in-session Get-ChildItem C:\Users\<upn> measurement; the User Storage tab in Intune likewise shows tier, not in-use.
  • Pool-level visibility is good, per-user used-bytes is not. usedStorageInGB at the pool level is accurate (it backs the alert rule), but it does not decompose to per-user.
  • No cross-policy aggregation. UES pools are scoped per policy assignment. A user on two policies (rare but possible) has two independent containers, neither of which sees the other.
  • Concurrent users on different CPCs in the same pool. Flex Shared is one-user-per-CPC, but a pool of N CPCs can have N users active simultaneously, each with their own UES profile attached on a different VM. Per-user projections must be computed individually; the policy-pool aggregate does not tell you which CPC any given user landed on.
  • Snapshot-reset breaks longitudinal disk trends. A freeStorageSpaceInBytes series on a Flex Shared CPC sawtooths around the snapshot-reset boundary, not the user-session boundary. Use cloudPcAuditEvent for reset events to segment the series.
  • OneDrive Files On-Demand can hydrate unpredictably. A user who opens a 30 GB folder pulls placeholders into local cache; the same user on the next session may show a clean cache. KFM size from Approach 2 is the cloud size, not the currently hydrated on the CPC size.
  • UES excludes the most volatile profile elements. The Packages\*\AC / LocalCache / TempState exclusions mean Edge / Teams / Outlook search index growth is a session-time concern but not a UES growth concern. That’s good for storage planning and bad for predicting peak in-session disk pressure.
  • Beta endpoints. cloudPCUserSettingsPersistence* lives under /beta and is subject to change. Pin a snapshot of the documented response shape in your tooling and re-verify quarterly.
  • Sov-cloud availability. The persistence endpoints are documented as global service only — not available in US Gov L4 / L5 (DOD) / China-21Vianet at time of writing. Customers in those clouds need a different reporting plan (likely Approach 4 + Approach 5 only).

Limitations and gotchas
#

  • No native W365 disk report. Setting customer expectations matters — there is no Cloud PC-specific tile to point at. Lead with Graph.

  • freeStorageSpaceInBytes defaults to 0. Per the Graph spec, the default value is 0 if the device has not reported. Treat any zero you see as “unknown,” not “full.” Filter rows where lastSyncDateTime is null or where both total and free are zero.

  • OS volume only. Both Graph properties measure the system drive. Additional data disks (rare on Cloud PC, more common on AVD-style configurations — see [[avd-os-disk-types]] for context) are not represented.

  • Sync latency. A user filling their Cloud PC in the last hour will not show the new value until the next Intune check-in. Force a Sync via Intune or a Graph syncDevice action when investigating an active incident.

  • Requires Intune enrollment. Cloud PCs that fail to enroll in Intune (rare, usually a provisioning policy + identity issue — see [[cloud-pc-provisioning-errors]]) emit no managedDevice record and therefore no storage values.

  • Flex Shared resets the disk. [[windows-365-flex|Windows 365 Flex]] in [[windows-365-flex-shared-mode|shared mode]] runs the [[windows-365-flex-snapshot-based-reset|snapshot-based reset]] on user sign-out, so free-space on a shared-mode Cloud PC reflects only the active session’s footprint, not cumulative user storage. Reporting against shared Flex Cloud PCs answers “is this session’s disk healthy right now?”, not “is this user storing too much?”

  • Resize doesn’t move data. A [[cloud-pc-resize|resize]] that grows the SKU also grows the OS disk. A Cloud PC that was full at 64 GB will report ample free space immediately after resize to 128 GB; trend dashboards spanning the resize need to normalize against totalStorageSpaceInBytes, not absolute free GB.

  • Frontline → Flex naming drift. Some Intune surfaces still label shared-mode Cloud PCs as “Frontline shared.” See [[cloud-pc-frontline-to-flex-rebrand]].

  • Privacy. freeStorageSpaceInBytes is treated as device telemetry, not user-content metadata, but reporting it alongside userPrincipalName does associate a personal usage pattern with an identified user. Apply the customer’s normal endpoint-monitoring data-handling rules.


Sources
#