PowerShell Folder Size Reporter with Top-N Largest Folders

Disk space disappears quietly. By the time a volume hits 95% capacity, the culprit — a log directory that grew unchecked, a video archive in the wrong location, an old VM snapshot folder — can take considerable time to locate manually. PowerShell has no Get-FolderSize cmdlet, but combining Get-ChildItem, Measure-Object, and a few safeguards around reparse points produces a fast, reliable folder-size reporter that ranks the biggest offenders across any path.
Quick Answer
Use Get-ChildItem -Path $root -Recurse -File -Attributes !ReparsePoint -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum to get total bytes for a folder, then wrap it across multiple paths and Sort-Object to find the top N.
Recursive Size Calculation with Get-ChildItem -Recurse and Measure-Object
The core pattern is straightforward. The key flags are -File (enumerate only files, not directory entries) and -Recurse (descend into subdirectories).
function Get-FolderSize {
param (
[string] $Path,
[switch] $SkipErrors
)
$ea = if ($SkipErrors) { "SilentlyContinue" } else { "Stop" }
$sizeBytes = Get-ChildItem -Path $Path `
-Recurse `
-File `
-Force `
-ErrorAction $ea |
Measure-Object -Property Length -Sum |
Select-Object -ExpandProperty Sum
[PSCustomObject]@{
Path = $Path
SizeBytes = $sizeBytes ?? 0
SizeGB = [math]::Round(($sizeBytes ?? 0) / 1GB, 3)
}
}
Get-FolderSize -Path "D:\Data" -SkipErrors
Handling Access Denied Errors Gracefully
On real servers, a recursive scan will hit System Volume Information, protected profile directories, and application sandboxes. Using -ErrorAction SilentlyContinue skips inaccessible paths silently; capturing the error record lets you report what was skipped.
$errorLog = [System.Collections.Generic.List[string]]::new()
$sizeBytes = Get-ChildItem -Path "C:\" `
-Recurse -File -Force `
-ErrorAction SilentlyContinue `
-ErrorVariable gciErrors |
Measure-Object -Property Length -Sum |
Select-Object -ExpandProperty Sum
foreach ($err in $gciErrors) {
$errorLog.Add($err.TargetObject)
}
Write-Host "Skipped $($errorLog.Count) inaccessible path(s)."
if ($errorLog.Count -gt 0) {
$errorLog | Set-Content "SkippedPaths.txt" -Encoding UTF8
}
The -ErrorVariable parameter collects all non-terminating errors into a named variable without suppressing them from the stream. This lets you log skipped paths for audit purposes without halting the scan.
Skipping Junction Points and Reparse Points with -Attributes
Reparse points — junction points, symbolic links, and volume mount points — can create circular references that send a recursive scan into an infinite loop, or count files multiple times by following a junction to a directory already included in the scan root.
# The -Attributes flag accepts a combination of FileAttributes values
# !ReparsePoint excludes any item tagged as a reparse point
$files = Get-ChildItem -Path "C:\Data" `
-Recurse `
-File `
-Force `
-Attributes !ReparsePoint `
-ErrorAction SilentlyContinue
# Verify no junction-point paths leaked through
$files | Where-Object { $_.Attributes -band [System.IO.FileAttributes]::ReparsePoint } |
Select-Object FullName # should be empty
This is the most important safety measure in any recursive file system script. Omitting it on a Windows Server with DFS junctions or SAN mount points can cause the script to run indefinitely.
Sorting and Returning the Top N Largest Folders
Scan immediate subdirectories of a root path — each treated as an independent unit — then rank them.
function Get-TopNFolders {
param (
[string] $RootPath,
[int] $TopN = 10
)
$subFolders = Get-ChildItem -Path $RootPath -Directory -Force `
-Attributes !ReparsePoint `
-ErrorAction SilentlyContinue
$results = foreach ($folder in $subFolders) {
Get-FolderSize -Path $folder.FullName -SkipErrors
}
$results | Sort-Object SizeBytes -Descending | Select-Object -First $TopN
}
Get-TopNFolders -RootPath "D:\Shares" -TopN 10
Formatting Output with Human-Readable Size Units
Raw byte counts are hard to scan. A formatting function that auto-selects the appropriate unit makes the output report-ready.
function Format-FileSize {
param ([long] $Bytes)
switch ($Bytes) {
{ $_ -ge 1TB } { return "{0:N2} TB" -f ($_ / 1TB) }
{ $_ -ge 1GB } { return "{0:N2} GB" -f ($_ / 1GB) }
{ $_ -ge 1MB } { return "{0:N2} MB" -f ($_ / 1MB) }
{ $_ -ge 1KB } { return "{0:N2} KB" -f ($_ / 1KB) }
default { return "$_ B" }
}
}
$top10 = Get-TopNFolders -RootPath "D:\Shares" -TopN 10
$top10 | ForEach-Object {
[PSCustomObject]@{
Rank = [array]::IndexOf($top10, $_) + 1
Path = $_.Path
Size = Format-FileSize -Bytes $_.SizeBytes
}
} | Format-Table -AutoSize
Rank Path Size
---- ---- ----
1 D:\Shares\VideoArchive 412.33 GB
2 D:\Shares\Backups 187.90 GB
3 D:\Shares\Installers 43.11 GB
Running Across Multiple Root Paths in Parallel
When scanning multiple drives or UNC paths, ForEach-Object -Parallel (PS7+) cuts total time significantly.
#Requires -Version 7
$rootPaths = @("C:\", "D:\", "E:\")
$allResults = $rootPaths | ForEach-Object -Parallel {
$root = $_
Get-ChildItem -Path $root -Directory -Force `
-Attributes !ReparsePoint `
-ErrorAction SilentlyContinue |
ForEach-Object {
$sz = (Get-ChildItem -Path $_.FullName -Recurse -File -Force `
-Attributes !ReparsePoint `
-ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum).Sum ?? 0
[PSCustomObject]@{ Path = $_.FullName; SizeBytes = $sz }
}
} -ThrottleLimit 3
$allResults | Sort-Object SizeBytes -Descending |
Select-Object -First 20 |
Export-Csv "TopFolders-$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation
Common Errors
- Junction points cause infinite loops: Without
-Attributes !ReparsePoint,Get-ChildItem -Recursefollows junction points and symbolic links indefinitely on systems with DFS, VSS snapshot directories, or volume mount points. Always include this attribute filter on any recursive file system scan. - Measure-Object -Sum on very large trees is slow: Accumulating millions of file objects in a pipeline before summing them requires significant memory. For very large trees (millions of files), consider a streaming running-total approach using a
foreachloop with a[long]accumulator, which avoids building the entire file object collection in memory.
Related Cmdlets / See Also
Wrapping Up
A reliable folder-size reporter needs three things: -Attributes !ReparsePoint to prevent infinite loops, -ErrorAction SilentlyContinue to survive permission denials, and human-readable size formatting for the output. Add parallel execution across multiple roots in PowerShell 7 and you have a fast, production-safe disk audit tool.


