I used VMware Workstation for a while which has an option to automatically compact the virtual hard disk after shutting down the VM, an excellent way of freeing up space on my laptop hard drive. But I switched to using only Hyper-V now, compacting virtual hard disks is not something you can enable in Hyper-V to run automatically. In this blog post, I will show you how to do this using a script that you can use to do that in an easy way 🙂
Limitations
Let me first start with things that this script can’t do, the optimize-vhd cmdlet has some limitations, and what the limitations are:
- Disks with checkpoints (Snapshots), the VM will have a .avhdx disk attached which should not be compacted
- VMs which use differencing disks which have child virtual disks associated with them
- Virtual hard disks that are associated with a virtual machine that has replication enabled and is currently involved in initial replication, resynchronization, test failover, or failover.
- I only tested this on my own machine, not a stand-alone Hyper-V server or a Hyper-V server running in a Fail-Over cluster
- The script only compacts .vhdx files, not .vhd files
- It only compacts dynamic .vhdx files, fixed disk files are not supported by the optimize-vhd cmdlet
- The VM needs to be turned off, disks can’t be compacted which belong to a running VM
- The script will only run locally on the computer running Hyper-V, it can’t connect to a remote Hyper-V instance
Running the script
After running, the Compact-VHDX function is available in your PowerShell session. You can run it without the -VMName parameter and it will try to compact all .vhdx files associated with the registered VMs on your computer. If you run it with the -VMName parameter and specify a certain VM, it will only compact the .vhdx(s) on that machine.
Example of running on all VMs
In this example, I ran the script on my laptop and it compacted .vhdx files on three VMs, you can see the space that it recovered in the column on the right: (1.656Gb)

Example of running on one VM
In this example, I ran the script on my laptop and specified the ‘Exam’ VM. You can see the space that it recovered in the column on the right, also 1.656Gb 🙂 (I used the same file that I copied into the VM and removed later so that there was some space to recover)

The script
Below is the script, you can save this locally and add this to your PowerShell profile by:
notepad $profile add ". c:\data\compact-vhdx.ps1" Close/Save and start a new PowerShell session
It has error checking/handling for:
- Is the Hyper-V PowerShell module installed, if not… Install it before continuing
- Is the VMName valid if specified?
- Is the disk Dynamic?
- Is the VM running?
- Errors during mounting/compacting/dismounting
function Compact-VHDX {
param (
[Parameter(Mandatory = $false, HelpMessage = "Enter the name of the machine from which space of the VHDX(s) should be recovered")][string]$VMName
)
#Validate if hyper-v module is available, install when needed
if (-not (Get-Module -listAvailable -Name Hyper-V)) {
Write-Warning "Hyper-V module is not installed, installing now..."
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Management-PowerShell -NoRestart:$true
}
#Check if specified VMName is valid if specified
if ($VMName) {
if (-not (Get-VM -Name $VMName -ErrorAction SilentlyContinue)) {
Write-Warning ("Specified VM {0} was not found, aborting..." -f $VMName)
return
}
}
#Validate if the Virtual machine specified is running, abort if yes
if ($VMName) {
if ((Get-VM -Name $VMName).State -eq 'Running') {
Write-Warning ("Specified VM {0} is found but is running, please shutdown VM first. Aborting..." -f $VMName)
return
}
}
#Validate if Virtual machines are running when $VMName was not specified, abort if yes
if (-not ($VMName)) {
if (Hyper-V\Get-VM | Where-Object State -eq Running) {
Write-Warning ("Hyper-V VM(s) are running, aborting...")
Write-Host ("Shutdown VM(s):") -ForegroundColor Red
hyper-v\get-vm | Where-Object State -eq Running | Select-Object Name, State | Sort-Object Name | Format-Table
return
}
}
#Gather all VHDXs from the VMs (or VM is $VMName was specified which don't have a parent/snapshot
if (-not ($VMName)) {
$vhds = Get-VM | Get-VMHardDiskDrive | Where-Object Path -Match '.vhdx' | Sort-Object VMName, Path
}
else {
$vhds = Get-VM $VMName | Get-VMHardDiskDrive | Where-Object Path -Match '.vhdx' | Sort-Object Path
}
if ($null -eq $vhds) {
Write-Warning ("No disk(s) found without parent/snapshot configuration, aborting....")
}
#Gather current size of VHDX files
$oldsize = foreach ($vhd in $vhds) {
if ((get-vhd $vhd.path).VhdType -eq 'Dynamic') {
[PSCustomObject]@{
VHD = $vhd.Path
OldSize = [math]::round((Get-Item $vhd.Path).Length / 1GB, 3)
}
}
}
#Compress all files
foreach ($vhd in $vhds) {
if ((get-vhd $vhd.path).VhdType -eq 'Dynamic') {
Write-Host ("nProcessing {0} from VM {1}..." -f $vhd.Path, $vhd.VMName) -ForegroundColor Gray
try {
Mount-VHD -Path $vhd.Path -ReadOnly -ErrorAction Stop
Write-Host "Mounting VHDX" -ForegroundColor Green
}
catch {
Write-Warning ("Error mounting {0}, please check access or if file is locked..." -f $vhd.Path )
continue
}
try {
Optimize-VHD -Path $vhd.Path -Mode Full
Write-Host ("Compacting VHDX") -ForegroundColor Green
}
catch {
Write-Warning ("Error compacting {0}, dismounting..." -f $vhd.Path)
Dismount-VHD $vhd.Path
return
}
try {
Dismount-VHD $vhd.Path -ErrorAction Stop
Write-Host ("Dismounting VHDX
") -ForegroundColor Green
}
catch {
Write-Warning ("Error dismounting {0}, please check Disk Management and manually dismount..." -f $vhd.Path)
return
}
}
}
#Report on new VHDX sizes
$report = foreach ($vhd in $vhds) {
if ((get-vhd $vhd.path).VhdType -eq 'Dynamic') {
[PSCustomObject]@{
'Old Size (Gb))' = ($oldsize | Where-Object VHD -eq $vhd.Path).OldSize
'New Size (Gb)' = [math]::round((Get-Item $vhd.Path).Length / 1GB, 3)
'Space recovered (Gb)' = ($oldsize | Where-Object VHD -eq $vhd.Path).OldSize - [math]::round((Get-Item $vhd.Path).Length / 1GB, 3)
VM = $vhd.VMName
VHD = $vhd.Path
}
}
}
#Show overview
if ($null -ne $report) {
return $report | Format-Table -AutoSize
}
else {
Write-Warning ("No dynamic disk(s) found to recover hard-disk space from, aborting....")
}
}
Download the script(s) from GitHub here