PowerShell Folder Size Reporter with Top-N Largest Folders
─□✕

PowerShell Folder Size Reporter with Top-N Largest Folders

PowerShell Tips Editor 3 min read
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 -Recurse follows 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 foreach loop 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.

Send-Item -To