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, State

The 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 -AutoSize

Example 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:10

In 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-ClusterPerf

To 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 LastHour

Values 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 -NoTypeInformation

Why

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-0 belong 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