If you have or need to use dynamically expanding VHDX files, when that file hits the upper ceiling of its allocated space, the file refuses to grow because it has obviously hit its maximum size. Depending on what's being stored in this file, it can either cause problems with applications trying to write to it, or if it's a server, it can cause problems with booting or day-to-day operation of the server.
It's also not an immediate problem because it's only when the file tries to grow that it gets a denied message. If it's not at its maximum size, it's still able to grow so you don't get an error. If it is at its maximum size but it's got a lot of whitespace in it, that is also not a problem until it's used up the whitespace.
Just because the file system reports 25GB, if the file has another 10GB to grow, you have no problem. The file system is only truly accurate as a benchmark with performance and service degradation when the amount of workspace matches the size of the file.
Visuals of the issue
This is the problem with some screenshots, here we can see a vhdx file at the size of 299MB - this is a test vhdx for this demo, this was the initial size
We can how see that the vhdx has grown to 465MB as below, however the used data is 42.4MB:
However we need to shrink this to the original size which was on creation as 165MB - however without a compact process this vxhd will not shrink, so lets run the script:
This will then shrink the file to the safest size that it can, which is 167MB in this instance as you can see below:
Existing scripts : Whitespace left inflated!
I recently faced this exact scenario: VHDX files consuming far more disk space than they should. I had a 25GB VHDX file that was only using 10GB of actual data, leaving 15GB of wasted "whitespace" that existing scripts simply couldn't reclaim.
After trying numerous scripts from the PowerShell Gallery and various GitHub repositories, I found myself increasingly frustrated. When you can't find a script that does exactly what you need to do, it's a lot easier to understand the requirements of what your script should be doing, follow best practices, and simply code your own. These tools would analyze the files correctly, identify the whitespace, but then fail to actually compact the files. Even worse, many required full Hyper-V installations just to manage simple VHDX files on file servers.
The breaking point came when I realized what was missing: a -Force parameter. Many of these scripts had conservative thresholds that prevented compaction unless there were massive amounts of whitespace. My 60% whitespace scenario was being ignored because it didn't meet their arbitrary size thresholds.
Where is this a problem?
This issue is particularly common in Remote Desktop Services environments with User Profile Disks (UPDs). These VHDX files grow dynamically as users add data, but they never shrink when data is deleted. Over time, you end up with bloated profile disks that waste significant storage space.
Coding custom script
I decided to write my own PowerShell script that addresses these shortcomings. The key requirements were:
- Work without Hyper-V tools - Perfect for file servers hosting UPD shares
- Accurate whitespace detection - Using actual file system analysis
- Flexible thresholds - Including a -Force option for any amount of whitespace
- Proper usage detection - Ensuring files aren't in use before attempting compaction
- UPD-aware - Specifically handling User Profile Disk scenarios
Mount and Analyze
# Mount the VHD to get detailed information
$mountResult = Mount-DiskImage -ImagePath $VHDPath -PassThru -ErrorAction Stop
if ($mountResult) {
# Get disk information
$diskImage = Get-DiskImage -ImagePath $VHDPath
$disk = $diskImage | Get-Disk
# Calculate actual used space across all partitions
$partitions = Get-Partition -DiskNumber $disk.Number
foreach ($partition in $partitions) {
$volume = Get-Volume -Partition $partition
if ($volume -and $volume.Size -gt 0) {
$usedSpace = $volume.Size - $volume.SizeRemaining
$totalUsedSpace += $usedSpace
}
}
}
File Detection Logic
# Determine if it's dynamic based on file size vs partition size
if ($vhdInfo.FileSize -lt ($totalPartitionSize * 0.95)) {
$vhdInfo.VhdType = "Dynamic"
# Calculate minimum possible size
$vhdInfo.MinimumSize = [math]::Max($totalUsedSpace + (100 * 1MB), $totalPartitionSize * 0.05)
} else {
$vhdInfo.VhdType = "Fixed"
}
Flexible Compaction Thresholds
# Allow compaction with flexible criteria
$canCompact = ($vhdInfo.VhdType -match "Dynamic" -and
($Force -or $whiteSpaceBytes -gt 50MB -or $whiteSpacePercent -gt 10) -and
-not $inUse)
User Profile Disk Detection
The script automatically detects UPD files and provides specific guidance:
# Check if it's a UPD file
$fileName = Split-Path $vhdxFile -Leaf
$isUPD = $fileName -match "^UVHD-.*\.vhdx$"
if ($isUPD) {
Write-Host " Profile Type: User Profile Disk (UPD)" -ForegroundColor Cyan
# Extract and display user SID
if ($fileName -match "UVHD-(.+)\.vhdx") {
$userSID = $matches[1]
Write-Host " User SID: $userSID" -ForegroundColor Cyan
}
}
Safe Compaction Using Built-in Tools
Rather than requiring Hyper-V tools, the script uses diskpart for the actual compaction:
function Invoke-VHDCompaction {
param([string]$VHDPath)
$tempScript = [System.IO.Path]::GetTempFileName()
# Create diskpart script for compaction
@"
select vdisk file="$VHDPath"
attach vdisk readonly
compact vdisk
detach vdisk
exit
"@ | Out-File -FilePath $tempScript -Encoding ASCII
# Execute compaction
$result = & diskpart /s $tempScript 2>&1
}
Conclusion
After struggling with existing solutions that either required full Hyper-V installations or failed to compact files with significant whitespace, I created a PowerShell script that actually works. The key insights were using built-in Windows tools, implementing flexible thresholds with a -Force option, and properly detecting file usage scenarios.
For administrators dealing with bloated VHDX files, this approach provides a practical solution that addresses the real-world limitations of existing tools while maintaining safety and reliability.