Incident
RMM alert came in at 7 AM: "C: drive below 2% free space." Application server, 99.2% capacity. Security patches failing to install. Event log flooded with "insufficient disk space" errors. Users were already reporting that the line-of-business application was crashing mid-save.
This was a 5-year-old server that had been upgraded in-place from Windows Server 2016 to 2019 to 2022, dragging years of accumulated cruft forward with every upgrade. The 256 GB C: drive had 1.8 GB free, and Windows was reporting it needed 4 GB minimum for the pending cumulative update. Ran standard Disk Cleanup as a first pass: recovered 400 MB. Not even close. We needed to find the other 40+ GB Windows was hiding.
Problem identification
- C: drive: 252 GB used of 256 GB, 1.8 GB free (0.7%)
- Windows Update: failing, needs 4 GB free for the pending cumulative update
- event log: 847 "disk full" errors in the past 24 hours
- application data: only 67 GB — not the problem
The math didn't add up. Windows itself was around 67 GB, application data was 67 GB, that's 134 GB accounted for. Where was the other 118 GB? Running TreeSize gave us the answer: C:\Windows\WinSxS was 48 GB, SoftwareDistribution was 23 GB, and there was a 19 GB Windows.old folder sitting from the 2019 to 2022 upgrade that had never been cleaned.
Why standard disk cleanup wasn't enough
The obvious solution was Disk Cleanup with "Clean up system files." Recovered 400 MB from temp files and thumbnails. Here's why it missed the real bloat:
- Windows.old: Disk Cleanup showed "Previous Windows Installation" but the checkbox was grayed out — file locks from running services kept it pinned
- SoftwareDistribution: Disk Cleanup only clears the Download subfolder (2 GB), not the DataStore (21 GB of accumulated update metadata)
- WinSxS: Disk Cleanup "marks" superseded components for removal but doesn't actually run DISM to delete them
- search index: 4.2 GB Windows.edb file — Disk Cleanup won't touch it at all
The bloat Windows doesn't want to clean by default was exactly what we needed to remove. We had to get aggressive.
Resolution
Ran our aggressive cleanup script via RMM. It goes well beyond what Disk Cleanup touches — stops services to release file locks, runs DISM directly for real WinSxS cleanup, and nukes the search index to force a clean rebuild.
$ErrorActionPreference = 'Stop'
<#
██╗ ██╗███╗ ███╗███████╗██╗ ██╗ █████╗ ██╗ ██╗██╗ ██╗
██║ ██║████╗ ████║██╔════╝██║ ██║██╔══██╗██║ ██║██║ ██╔╝
██║ ██║██╔████╔██║█████╗ ███████║███████║██║ █╗ ██║█████╔╝
██║ ██║██║╚██╔╝██║██╔══╝ ██╔══██║██╔══██║██║███╗██║██╔═██╗
███████╗██║██║ ╚═╝ ██║███████╗██║ ██║██║ ██║╚███╔███╔╝██║ ██╗
╚══════╝╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚═╝
================================================================================
SCRIPT : Disk Cleanup v1.2.3
AUTHOR : Limehawk.io
DATE : January 2026
USAGE : .\disk_cleanup.ps1
================================================================================
FILE : disk_cleanup.ps1
DESCRIPTION : Runs Windows Disk Cleanup plus SoftwareDistribution/WinSxS cleanup
--------------------------------------------------------------------------------
README
--------------------------------------------------------------------------------
PURPOSE
Runs Windows Disk Cleanup utility on all fixed local drives, plus targeted
cleanups for SoftwareDistribution, WinSxS, and Search Index to free up disk space.
Shows disk space before/after and progress updates during long-running operations.
DATA SOURCES & PRIORITY
1) Hardcoded values (CleanupProfile defined within the script body)
2) System query (WMI to enumerate fixed drives)
REQUIRED INPUTS
- CleanupProfile : 'verylow' (hardcoded) - Determines cleanup aggressiveness.
- CleanmgrTimeout : 3600 (hardcoded) - Maximum seconds to wait for cleanmgr.
- StatusInterval : 60 (hardcoded) - Seconds between progress updates.
SETTINGS
- DriveType filter: 3 (fixed local disks only)
- Cleanups run sequentially to avoid conflicts
- DISM targets system-wide (C: only)
- Progress updates shown every 60 seconds during cleanmgr
BEHAVIOR
1. Checks/elevates to admin
2. Queries WMI for fixed drives
3. Shows free space before cleanup
4. Runs cleanmgr.exe per drive with /verylowdisk flag (with progress updates)
5. Performs targeted cleanups on C: (SoftwareDistribution, WinSxS, Search Index)
6. Shows free space after cleanup and total freed
PREREQUISITES
- Windows 10/11 with cleanmgr.exe and DISM
- WMI service running
- Admin privileges for DISM/services
SECURITY NOTES
- No secrets in logs
- Targeted deletions only (no user files)
EXIT CODES
- 0: Success
- 1: Failure (validation/WMI/admin check)
EXAMPLE RUN
[INFO] INPUT VALIDATION
==============================================================
Cleanup Profile : verylow
Cleanmgr Timeout : 1800 seconds
Status Interval : 60 seconds
Running as Admin : True
[RUN] DISK CLEANUP
==============================================================
Querying fixed local drives...
Found 2 drive(s) : C:, D:
[RUN] CLEANUP DRIVE C:
==============================================================
Free space before : 45.2 GB
Running cleanmgr /verylowdisk (this may take 10-30 minutes)...
[still running... 1 min elapsed]
[still running... 2 min elapsed]
Free space after : 52.8 GB
Freed : 7.6 GB
[RUN] TARGETED CLEANUPS
==============================================================
Running SoftwareDistribution cleanup...
Completed (folder cleared)
Running WinSxS cleanup via DISM...
Completed (superseded components removed)
Rebuilding Search Index...
Completed (DB deleted; will rebuild automatically)
[INFO] RESULT
==============================================================
Status : Success
Drives Processed : 2 of 2
Targeted Cleanups : 3 of 3
Total Freed : 8.4 GB
[OK] FINAL STATUS
==============================================================
Disk cleanup completed successfully. Reboot recommended for full effect.
[OK] SCRIPT COMPLETED
==============================================================
--------------------------------------------------------------------------------
CHANGELOG
--------------------------------------------------------------------------------
2026-01-29 v1.2.3 Increase cleanmgr timeout from 30 to 60 minutes per drive
2026-01-29 v1.2.2 Fix Get-FreeSpaceBytes calls using incorrect named parameter
2026-01-29 v1.2.1 Increased script timeout to 180 minutes in metadata
2026-01-29 v1.2.0 Added progress updates and disk space reporting during cleanup
2025-12-23 v1.1.1 Updated to Limehawk Script Framework
2025-11-29 v1.1.0 Added SoftwareDistribution, DISM WinSxS, Search Index; admin check
2025-10-28 v1.0.0 Initial implementation
================================================================================
#>
Set-StrictMode -Version Latest
# ==== STATE ====
$errorOccurred = $false
$errorText = ""
$processedCount = 0
$targetedCleanups = 0
$totalTargeted = 3 # SoftwareDist, WinSxS, Search
$totalFreedBytes = 0
# ==== HARDCODED INPUTS ====
$CleanupProfile = 'verylow'
$CleanmgrTimeout = 3600 # 60 minutes max per drive
$StatusInterval = 60 # Show status every 60 seconds
# ==== HELPER FUNCTIONS ====
function Get-FreeSpaceBytes {
$DriveLetter = $args[0]
$drive = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$DriveLetter'" -ErrorAction SilentlyContinue
if ($drive) { return $drive.FreeSpace }
return 0
}
function Format-Bytes {
$Bytes = [long]$args[0]
if ($Bytes -ge 1GB) { return "{0:N1} GB" -f ($Bytes / 1GB) }
if ($Bytes -ge 1MB) { return "{0:N1} MB" -f ($Bytes / 1MB) }
if ($Bytes -ge 1KB) { return "{0:N1} KB" -f ($Bytes / 1KB) }
return "$Bytes bytes"
}
# ==== ADMIN CHECK ====
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
if (-not $isAdmin) {
Write-Host ""
Write-Host "[ERROR] ERROR OCCURRED"
Write-Host "=============================================================="
Write-Host "Script requires admin privileges for DISM and services."
Write-Host "Please relaunch as Administrator."
exit 1
}
# ==== VALIDATION ====
if ([string]::IsNullOrWhiteSpace($CleanupProfile)) {
$errorOccurred = $true
$errorText += "- CleanupProfile is required."
}
if ($errorOccurred) {
Write-Host ""
Write-Host "[ERROR] ERROR OCCURRED"
Write-Host "=============================================================="
Write-Host $errorText
exit 1
}
# ==== RUNTIME OUTPUT ====
Write-Host ""
Write-Host "[INFO] INPUT VALIDATION"
Write-Host "=============================================================="
Write-Host "Cleanup Profile : $CleanupProfile"
Write-Host "Cleanmgr Timeout : $CleanmgrTimeout seconds"
Write-Host "Status Interval : $StatusInterval seconds"
Write-Host "Running as Admin : $isAdmin"
Write-Host ""
Write-Host "[RUN] DISK CLEANUP"
Write-Host "=============================================================="
# Query fixed local drives
Write-Host "Querying fixed local drives..."
try {
$drives = Get-CimInstance Win32_LogicalDisk -ErrorAction Stop | Where-Object { $_.DriveType -eq 3 }
if ($null -eq $drives -or @($drives).Count -eq 0) {
$errorOccurred = $true
$errorText += "- No fixed local drives found."
} else {
$driveList = ($drives | ForEach-Object { $_.DeviceID }) -join ', '
$driveCount = @($drives).Count
Write-Host "Found $driveCount drive(s) : $driveList"
}
} catch {
$errorOccurred = $true
$errorText += "- CIM query failed: $($_.Exception.Message)"
}
if ($errorOccurred) {
Write-Host ""
Write-Host "[ERROR] ERROR OCCURRED"
Write-Host "=============================================================="
Write-Host $errorText
exit 1
}
# Run cleanmgr for each drive with progress reporting
foreach ($drive in $drives) {
$driveLetter = $drive.DeviceID
Write-Host ""
Write-Host "[RUN] CLEANUP DRIVE $driveLetter"
Write-Host "=============================================================="
# Get free space before
$freeSpaceBefore = Get-FreeSpaceBytes $driveLetter
Write-Host "Free space before : $(Format-Bytes $freeSpaceBefore)"
Write-Host "Running cleanmgr /verylowdisk (this may take 10-30 minutes)..."
try {
# Start cleanmgr as background process
$cleanmgrProcess = Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/verylowdisk", "/d", $driveLetter -PassThru -WindowStyle Hidden -ErrorAction Stop
# Poll for completion with status updates
$elapsed = 0
while (-not $cleanmgrProcess.HasExited) {
Start-Sleep -Seconds $StatusInterval
$elapsed += $StatusInterval
$elapsedMin = [math]::Floor($elapsed / 60)
Write-Host " [still running... $elapsedMin min elapsed]"
if ($elapsed -ge $CleanmgrTimeout) {
Write-Host " [WARN] Timeout reached ($CleanmgrTimeout seconds). Stopping cleanmgr..."
$cleanmgrProcess | Stop-Process -Force -ErrorAction SilentlyContinue
break
}
}
# Get free space after
$freeSpaceAfter = Get-FreeSpaceBytes $driveLetter
$freedBytes = $freeSpaceAfter - $freeSpaceBefore
$totalFreedBytes += $freedBytes
Write-Host "Free space after : $(Format-Bytes $freeSpaceAfter)"
if ($freedBytes -gt 0) {
Write-Host "Freed : $(Format-Bytes $freedBytes)"
} elseif ($freedBytes -lt 0) {
Write-Host "Note : Space decreased (files may have been created during cleanup)"
} else {
Write-Host "Freed : 0 bytes (drive was already clean)"
}
$processedCount++
} catch {
Write-Host "[WARN] cleanmgr failed for $driveLetter : $($_.Exception.Message)"
}
}
# Targeted cleanups (C: only)
if ($drives | Where-Object { $_.DeviceID -eq 'C:' }) {
Write-Host ""
Write-Host "[RUN] TARGETED CLEANUPS"
Write-Host "=============================================================="
# 1. SoftwareDistribution
Write-Host "Running SoftwareDistribution cleanup..."
try {
$services = @('wuauserv', 'cryptSvc', 'bits', 'msiserver')
foreach ($svc in $services) { Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue }
Remove-Item -Path "C:\Windows\SoftwareDistribution\*" -Recurse -Force -ErrorAction SilentlyContinue
foreach ($svc in $services) { Start-Service -Name $svc -ErrorAction SilentlyContinue }
$targetedCleanups++
Write-Host " Completed (folder cleared)"
} catch {
Write-Host " [WARN] $($_.Exception.Message)"
}
# 2. WinSxS via DISM
Write-Host "Running WinSxS cleanup via DISM..."
try {
$dismProcess = Start-Process -FilePath "Dism.exe" -ArgumentList "/Online", "/Cleanup-Image", "/StartComponentCleanup" -Wait -NoNewWindow -PassThru -ErrorAction Stop
if ($dismProcess.ExitCode -eq 0) {
$targetedCleanups++
Write-Host " Completed (superseded components removed)"
} else {
Write-Host " [WARN] DISM exited with code $($dismProcess.ExitCode)"
}
} catch {
Write-Host " [WARN] $($_.Exception.Message)"
}
# 3. Search Index Rebuild
Write-Host "Rebuilding Search Index..."
try {
Stop-Service -Name "WSearch" -Force -ErrorAction SilentlyContinue
Remove-Item -Path "C:\ProgramData\Microsoft\Search\Data\Applications\Windows\Windows.edb" -Force -ErrorAction SilentlyContinue
Start-Service -Name "WSearch" -ErrorAction SilentlyContinue
$targetedCleanups++
Write-Host " Completed (DB deleted; will rebuild automatically)"
} catch {
Write-Host " [WARN] $($_.Exception.Message)"
}
} else {
Write-Host ""
Write-Host "[WARN] C: drive not found; skipping targeted cleanups."
}
Write-Host ""
Write-Host "[INFO] RESULT"
Write-Host "=============================================================="
$overallStatus = if ($processedCount -eq $driveCount -and $targetedCleanups -eq $totalTargeted) { "Success" } else { "Partial Success" }
Write-Host "Status : $overallStatus"
Write-Host "Drives Processed : $processedCount of $driveCount"
Write-Host "Targeted Cleanups : $targetedCleanups of $totalTargeted"
Write-Host "Total Freed : $(Format-Bytes $totalFreedBytes)"
Write-Host ""
if ($overallStatus -eq "Success") {
Write-Host "[OK] FINAL STATUS"
Write-Host "=============================================================="
Write-Host "Disk cleanup completed successfully for all drives and targets."
Write-Host "Reboot recommended to finalize changes."
} else {
Write-Host "[WARN] FINAL STATUS"
Write-Host "=============================================================="
Write-Host "Disk cleanup completed with warnings. Check output above."
}
Write-Host ""
Write-Host "[OK] SCRIPT COMPLETED"
Write-Host "=============================================================="
exit 0
Why this works
The script attacks the four major space hogs that standard cleanup misses. What gets cleaned:
- cleanmgr /verylowdisk: runs Disk Cleanup in emergency mode — auto-selects all safe options without prompting
- SoftwareDistribution: stops Windows Update services, deletes ENTIRE folder contents (not just Download), restarts services
- DISM /StartComponentCleanup: actually removes superseded WinSxS components (Disk Cleanup only marks them)
- Windows.edb: deletes the search index database, forcing a clean rebuild
Key insight: the real space savings come from WinSxS component cleanup. On this server, DISM recovered 31 GB of superseded system file versions accumulated from 5 years of in-place upgrades — files Windows was keeping "just in case" but would never actually use.
Outcome
Script ran for 23 minutes (DISM is slow, and that's normal — it's walking every component in the store). Total space recovered: 47.3 GB. C: drive went from 0.7% free to 19.2% free. Rebooted the server, the pending Windows Update installed successfully, application crashes stopped. Server has been running clean ever since.
- space recovered: 47.3 GB from Windows bloat
- free space after: 49.1 GB (19.2% of C:)
- script runtime: 23 minutes (mostly DISM)