TL;DR: Storage Spaces Direct and Azure Local clusters automatically record per-VM network bandwidth, storage IOPS, and storage latency in Cluster Performance History. The script below turns that history into one row per VM showing the average and peak for the last hour, day, or week — and when each VM's worst latency occurred — so you can find out what a VM was doing during a slowdown that has already passed.
Need what is happening right now instead? Use the live companion articles: Get-StorageQoSFlow for per-VM storage, and the Hyper-V vNIC counters for per-VM network (including dropped packets). For the latest Cluster Performance History sample of a single VM, see step 3. Every value in all three articles is an average over some interval — a few seconds for the live views, 30 seconds to 5 minutes or more for history — so the choice is how fine a window you need, not live versus averaged.
Recommended action
Where to run: An elevated PowerShell window directly on a cluster node (console or Remote Desktop). Do not run it from inside an Enter-PSSession to a node: the script contacts every node to list its VMs, and from inside a remote session that is a second hop, which Windows blocks by default.
Each command is labeled with where to run it and what it covers: all VMs in the cluster or one VM.
1. Confirm performance history is running
Run on: any one cluster node, directly
Covers: the whole cluster
Performance history depends on the cluster's Health resource and a dedicated ClusterPerformanceHistory volume. If either is missing, the commands below return nothing.
Get-ClusterResource | Where-Object ResourceType -eq 'Health Service' | Format-Table Name, State, OwnerNode
Get-ClusterSharedVolume | Where-Object Name -like '*ClusterPerformanceHistory*' | Format-Table Name, StateThe Health resource should be Online and the volume should be listed. If the volume is missing, run Start-ClusterPerformanceHistory and allow a minute for it to be created. History starts accumulating from that point, so earlier periods will not be available.
2. Per-VM report: average and peak over a time window
Run on: any one cluster node, directly
Covers: all VMs in the cluster — run it once, not per node
Set $timeFrame on the first line to the window you want. VMs with no activity in the window (for example, VMs that were powered off) are left out.
$timeFrame = 'LastDay' # LastHour, LastDay, LastWeek, LastMonth, LastYear
$series = 'VMNetworkAdapter.Bandwidth.Inbound', 'VMNetworkAdapter.Bandwidth.Outbound',
'VHD.IOPS.Total', 'VHD.Latency.Average'
function Get-Stat($s, $name, $scale) {
$v = @($s[$name] | ForEach-Object Value)
if ($v.Count -eq 0) { return 0, 0 }
$m = $v | Measure-Object -Average -Maximum
[math]::Round($m.Average * $scale, 2), [math]::Round($m.Maximum * $scale, 2)
}
$vms = Get-VM -ComputerName (Get-ClusterNode).Name
$report = foreach ($vm in $vms) {
$p = $vm | Get-ClusterPerf -VMSeriesName $series -TimeFrame $timeFrame -ErrorAction SilentlyContinue
if (-not $p) { continue }
$s = $p | Group-Object { $_.MetricId.Split(',')[0] } -AsHashTable -AsString
$rx = Get-Stat $s 'VMNetworkAdapter.Bandwidth.Inbound' 1e-6
$tx = Get-Stat $s 'VMNetworkAdapter.Bandwidth.Outbound' 1e-6
$io = Get-Stat $s 'VHD.IOPS.Total' 1
$lat = Get-Stat $s 'VHD.Latency.Average' 1000
$worst = $s['VHD.Latency.Average'] | Sort-Object Value -Descending | Select-Object -First 1
[PSCustomObject]@{
VM = $vm.Name
Host = $vm.ComputerName
'Rx Mbps avg' = $rx[0]; 'Rx Mbps peak' = $rx[1]
'Tx Mbps avg' = $tx[0]; 'Tx Mbps peak' = $tx[1]
'IOPS avg' = $io[0]; 'IOPS peak' = $io[1]
'Lat ms avg' = $lat[0]; 'Lat ms peak' = $lat[1]
'Lat peak time' = if ($worst -and $worst.Value -gt 0) { $worst.Time } else { $null }
}
}
$report = $report | Where-Object {
($_.PSObject.Properties | Where-Object Name -notin 'VM', 'Host', 'Lat peak time' |
Measure-Object Value -Sum).Sum -gt 0 }
$report | Sort-Object 'IOPS avg' -Descending | Format-Table -AutoSizeExample output (LastDay):
VM Host Rx Mbps avg Rx Mbps peak Tx Mbps avg Tx Mbps peak IOPS avg IOPS peak Lat ms avg Lat ms peak Lat peak time
-- ---- ----------- ------------ ----------- ------------ -------- --------- ---------- ----------- -------------
APP-SQL01 NODE2 12.40 88.31 30.12 142.70 410.35 2321.54 0.42 1.87 10/13 02:15
FILE01 NODE1 5.26 24.61 1.27 5.61 110.35 1321.54 0.36 0.67 10/13 23:40
DC01 NODE1 0.08 8.41 0.01 0.38 13.02 175.72 0.28 0.37 10/14 06:05
MGMT01 NODE2 0 0.01 0 0.06 0.98 11.11 0.76 41.20 10/14 03:10In this example, MGMT01 is normally quiet (0.76 ms average) but hit 41.2 ms at 03:10 — a single event worth matching against that VM's backup or patching schedule.
To sort by a different column, change the last line, for example Sort-Object 'Lat ms peak' -Descending.
3. One VM: latest sample, or every sample over time
Run on: the cluster node currently hosting the VM, directly
Covers: one VM
For the most recent sample of every series for the VM — network, storage, CPU, and memory — leave out -TimeFrame:
Get-VM -Name "VMName" | Get-ClusterPerfTo list every sample in a window, so you can see exactly when a spike started and ended, add a series and a time frame:
Get-VM -Name "VMName" | Get-ClusterPerf -VMSeriesName "VHD.Latency.Average" -TimeFrame LastHourValues in this view are already converted to readable units. Replace the series name with any from the list in Optional details, such as VMNetworkAdapter.Bandwidth.Inbound or VHD.IOPS.Total.
4. Optional: save the report
Run on: the same PowerShell session as step 2
Covers: all VMs in the cluster, from the step 2 report
The file is written to the node you are running on. The C:\Temp folder must already exist.
$report | Export-Csv -Path C:\Temp\VM-PerfHistory-$timeFrame-$(Get-Date -Format yyyyMMdd-HHmm).csv -NoTypeInformationWhy
The cluster's Health Service samples performance counters for every VM, virtual disk, volume, drive, and network adapter, and stores them on the ClusterPerformanceHistory volume. This is the same data behind the performance charts in Windows Admin Center. Because the history is kept by the cluster rather than by each node, Get-ClusterPerf can return it for any VM regardless of where it ran.
At the VM level, the network values are the total across all of the VM's network adapters, and the storage values are the total across all of its virtual disks.
Key columns:
- Rx / Tx Mbps — network traffic received and sent by the VM, in megabits per second.
- IOPS — storage I/O operations per second across the VM's virtual disks.
- Lat ms — average storage latency in milliseconds.
- Lat peak time — when the VM's worst storage latency in the window occurred.
Going forward
- Peaks are peaks of averages. Each stored sample is already an average over its interval: 30 seconds for LastHour and 5 minutes for LastDay, with coarser intervals for longer windows. A burst shorter than the interval is smoothed out. For short spikes, use LastHour, or the live companion articles while the problem is happening.
- History only exists from when it started. If performance history was just enabled or recreated, longer windows will be incomplete.
- A VM that shows network data but zero storage may have its virtual disks outside cluster storage. Check where its disks are stored before treating it as a problem.
- On Azure Local, infrastructure VMs appear in the list. Names such as
...-control-plane-0belong to the Azure Arc resource bridge or AKS and are expected. - For per-disk, per-adapter, or dropped-packet detail, use the live companion articles — history is kept per VM only.
Optional details
Available VM series. To list them for any VM, run Get-VM -Name "VMName" | Get-ClusterPerf. They include:
- Network:
VMNetworkAdapter.Bandwidth.Inbound,.Outbound,.Total - Storage:
VHD.IOPS.Read,.Write,.Total;VHD.Latency.Average;VHD.Throughput.Read,.Write,.Total - CPU and memory:
VM.Cpu.Usage,VM.Memory.Assigned,VM.Memory.Pressure, and others
Raw units. The interactive view (step 3) auto-scales units for display. The underlying Value property is always in base units — bits per second for network, seconds for latency, bytes per second for throughput — which is why the report script multiplies by 1e-6 and 1000.
Microsoft reference: Performance history for Azure Local