This article provides PowerShell code and examples to analyze Windows Storage Spaces Direct pools. The tool can return the current storage capacity, free space, and other values including the amount of free space required to automatically rebuild a failed disk.
Purpose
Monitoring Storage Spaces pools for capacity and available free space is simple enough with Windows Server Manager. Selecting the Storage Pool properties provides a graphical report.

Retrieving these values from PowerShell takes a bit more work. What is required for this operation is to:
- Select the Storage Pool
- Retrieve every disk within this pool
- Sum up all the capacity and free space on each disk
A trickier calculation is to evaluate if enough un-allocated space is available to survive a disk failure and automatically rebuild the storage pool to map around the missing disk. This is even more complex when there are multiple disk media types within your pool. An SSD failure can only be re-mapped with other SSD drives.
Get-ClusterS2DStorageUsage
The CmdLet included in this article provides a simple command to analyze pools and programmatically return pool statistics.
Command Syntax:
Get-ClusterS2DStorageUsage [-StoragePool string[ ] [-MediaType SSD|HDD|SCM]
Parameters:
-StoragePool
Storage Pool friendly name. If not included all non-primordial storage pools will be processed.
-MediaType
Selects specific disk drive media types for processing. If not included, all non-Journal media types processed.
Example 1: Basic output
Get-ClusterS2DStorageUsage
Pool : S2DPool
TotalCapacity : 120
TotalUsedSpace : 24
TotalFreeSpace : 96
PercentUsed : 20
UnitOfMeasure : GB
DiskCount : 24
SelectedMediaType : ALL
ReserveRequired : 5
Example 2: Disk media type selected
Get-ClusterS2DStorageUsage -MediaType HDD | ft Pool, DiskCount, TotalCapacity, TotalFreeSpace, ReserveRequired
Pool DiskCount TotalCapacity TotalFreeSpace ReserveRequired
---- ---------- ------------ -------------- ---------------
S2DPool 16 80 64 5
The results of this command show the S2DPool has 16 HDD disks with 64GB free space and required 5GB free space to rebuild.
Example 3: Verbose output
Get-ClusterS2DStorageUsage -Verbose
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE: Disk Name: [Msft Virtual Disk] Size: 5.00 GB Alloc: 1.00 GB Free Space: 4.00 GB
VERBOSE:
VERBOSE: -----------------------------------------------------
VERBOSE: Pool: S2DPool
VERBOSE:
VERBOSE: Total Space: 120.00 GB
VERBOSE: Total Alloc: 24.00 GB
VERBOSE: Free Space: 96.00 GB
VERBOSE: Percent Used: 20.00% GB
Automation
Since the CmdLet returns an object, the return values can be used for automation and DevOps scripts.
For example, the following script:
$Usage = Get-ClusterS2DStorageUsage -StoragePool S2DPool
if ($Usage.ReserveRequired -gt $Usage.TotalFreeSpace) { Write-Host "Call Home for Help!" }
Checks the S2DPool Storage Pool has enough excess capacity to rebuild.
This can be used in operations monitoring systems such as PowerShell Desired State Configuration (DSC) environments or potentially any monitoring environment.
Or potentially each disk drive media type can be tested as each disk type is likely to be different capacities.
$Usage = Get-ClusterS2DStorageUsage -StoragePool S2DPool -MediaType SSD
if ($Usage.ReserveRequired -gt $Usage.TotalFreeSpace)
{ Write-Host "Warning, SSD capacity below requirement to rebuild!" }
If ($Usage.PercentUsed -gt “90”)
{ Write-Host "Warning, low on SSD space!" }
PowerShell CmdLet
The following code should be copied into a file called Get-ClusterS2DStorageUsage.psm1 and installed like any PowerShell module. The CmdLet can be used for testing.
function Get-ClusterS2DStorageUsage {
<# .SYNOPSIS Return the Pool Storage Usage .DESCRIPTION Describe the function in more detail .EXAMPLE Give an example of how to use it .EXAMPLE Give another example of how to use it .PARAMETER poolname The computer name to query. Just one. .PARAMETER computername The name computer name to query #>
[CmdletBinding()]
param
(
[Parameter(
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='- FriendlyName ')]
[Alias('StoragePool')]
[string[]]$FriendlyName,
[string]$MediaType = 'all'
)
begin
{
$MediaType = $MediaType.ToUpper()
Write-Verbose "Checking Cluster Nodes exists"
$ClusterNodes = Get-ClusterNode
if ($ClusterNodes) {
$Msg = $ClusterNodes.Count.ToString() + " cluster nodes found"
Write-Verbose $Msg
} else {
$Msg = "No cluster nodes found"
Write-Error $Msg
Return
}
if (! $FriendlyName) {
Write-Verbose "No StoragePool name entered - Getting all Pools"
$SP = Get-StoragePool | ? IsPrimordial -eq $False
if ($SP) {
$FriendlyName = $SP.FriendlyName
$Msg = "Storage Pool found [" + $FriendlyName + "]"
Write-Verbose $Msg
} else {
Write-Error "Error - No Storage Pools available"
}
}
$Obj = @()
}
Process
{
foreach ($Pool in $FriendlyName)
{
$Msg = "Processing " + $Pool
Write-Verbose $Msg
Write-Verbose "Checking StoragePool exists"
$thisPool = Get-StoragePool -FriendlyName $Pool -ErrorAction Stop
Write-Verbose "Checking for disks with the MediaType"
if ( $MediaType -eq "all" )
{
$CapacityDisks = $thisPool | Get-PhysicalDisk | ? MediaType -ne "Journal"
} else {
$CapacityDisks = $thisPool | Get-PhysicalDisk | ? MediaType -eq $MediaType
}
if ($CapacityDisks) {
$Msg = $CapacityDisks.Count.ToString() + " Disks with MediaType " + $MediaType + " found"
Write-Verbose $Msg
} else {
$Msg = "No qualifying disks found with MediaType:" + $MediaType
Write-Error $Msg
Return
}
$PoolDiskCount = $CapacityDisks.Count
$Msg = ("Number of Pool Disks: " + $PoolDiskCount)
Write-Verbose $Msg
$TotalSize = 0
$TotalAlloc = 0
ForEach ($Disk in $CapacityDisks)
{
$DiskSize = $Disk.Size /1GB
$DiskAlloc = $Disk.AllocatedSize /1GB
$FreeSpace = $DiskSize - $DiskAlloc
$TotalSize += $DiskSize
$TotalAlloc += $DiskAlloc
$Msg = "Disk Name: [" + $Disk.FriendlyName + "] ",
("Size: {0:n2} GB " -f ($DiskSize) ),
("Alloc: {0:n2} GB " -f ($DiskAlloc)),
("Free Space: {0:n2} GB" -f ($FreeSpace))
Write-Verbose $Msg
}
$PoolFree = $TotalSize - $TotalAlloc
$PercentFull = $TotalAlloc / $TotalSize * 100
Write-Verbose " "
Write-Verbose "-----------------------------------------------------"
$Msg = "Pool: " + $thisPool.FriendlyName
Write-Verbose $Msg
Write-Verbose " "
$Msg = ("Total Space: {0:n2} GB" -f ($TotalSize))
Write-Verbose $Msg
$Msg = ("Total Alloc: {0:n2} GB" -f ($TotalAlloc))
Write-Verbose $Msg
$Msg = ("Free Space: {0:n2} GB" -f ($PoolFree))
Write-Verbose $Msg
$Msg = ("Percent Used: {0:n2}% GB" -f ($PercentFull))
Write-Verbose $Msg
$ReserveRequired = $TotalSize / $PoolDiskCount
$Properties = [Ordered]@{
'Pool'=$thisPool.FriendlyName;
'TotalCapacity'=$TotalSize;
'TotalUsedSpace'=$TotalAlloc;
'TotalFreeSpace'=$PoolFree;
'PercentUsed'=$PercentFull
'UnitOfMeasure'="GB";
'DiskCount'=$PoolDiskCount;
'SelectedMediaType'=$MediaType;
'ReserveRequired'=$ReserveRequired;
}
$Obj += New-Object –Typename PSObject –Property $Properties
}
Write-Output $Obj
}
}
Please do provide feedback on errors or any other comments below.

not sure what version of windows server you are using in this article…..but…
$CapacityDisks = $thisPool | Get-PhysicalDisk | ? MediaType -ne “Journal”
MediaType doesn’t exist for me. I had to change this to “Usage”
scratch that. it works but I wrongly presumed that the NVMe drives should not be included in my results if I had three tiers. I see that they are included in “all”
I modified my script to not included NVMe in the default (run without -MediaType parameter) and only include them if I add -MediaType all)
I did this since the journal drives are not really to be counted in space calculations from my understanding.
Hi Matt
Please excuse the very late reply.
You are correct, the script does not calculate correctly for a 3 tier configuration.
And you are correct journal disks are not included in capacity, but NVNe and NVDIMM SCM drives can be capacity drives.
It also does not take into account configurations which have multiple disk size/performances.
Multiple types of capacity disks are valid but more complex to administer.
We will fix and add the 3 tier options, and simply note the multi-capacity onfigurations.
also, the reserve required doesn’t seem to match (at least in my case) the recommended reserve of one disk per node (up to four disks) of each disk type.
https://docs.microsoft.com/en-us/windows-server/storage/storage-spaces/plan-volumes#choosing-how-many-volumes-to-create
FTA: We recommend reserving the equivalent of one capacity drive per server, up to 4 drives. You may reserve more at your discretion, but this minimum recommendation guarantees an immediate, in-place, parallel repair can succeed after the failure of any drive.
can you explain your reasoning for the usage of:
$ReserveRequired = $TotalSize / $PoolDiskCount
it should be something more like (pseudo-ish code):
ReserveRequiredHDD = PoolDiskCount where type is HDD and not journal
ReserveRequiredSSD = PoolDiskCount where type is SSD and not journal
ReserveRequired = ReserveRequiredHDD + ReserveRequiredSSD
I didn’t add the logic required to account for one drive type per node up to a total of four.
the reason I ask is that my results from running your script were much lower than what every vendor has suggested I have for reserve space. I think it is that you are dividing by the total number of disks thus really only allowing for the capacity of the average of one of each drive type. in my case I have four 10TB HDD and four 1.9TB SSD per node in a six node cluster. the resultant reserve from your script said I should have 5551.125 which is roughly the average of one of each disk type combined. however, every vendor has recommended a minimum of 46TB which is the sum of 4 of each disk type.
Hi Matt
You are exactly right.
For a configuration with 4 10TB disks, you need at least 10TB reserve space.
This script does not account for configurations with multiple types or sizes of capacity disks.
I you have a configuration like this, you have to reserve 10TB per node.
1 disk per node is the recommendation.