Disk cleanup and maintenance automation
Overview
Section titled “Overview”This article classifies scheduled disk cleanup tasks on Windows Server into the following four categories, and explains how to automate each of them using dedicated PowerShell scripts:
- WinSxS optimization (reducing the component store)
- Temp folder cleanup (based on retention days)
- Log file rotation (multiple paths with per-path retention)
- Event log backup and clear
Variable conventions
Section titled “Variable conventions”| Variable | Example | Note |
|---|---|---|
<<ADMIN_USER>> | Administrator | User account that runs the scheduled tasks |
<<LOG_PATH>> | C:\Maintenance\Logs | Common log directory for all scripts |
<<BACKUP_PATH>> | C:\Maintenance\Backups | Directory for event log backups |
<<DAILY_TEMP_TASK_NAME>> | DailyTempCleanup | Task name for Temp cleanup |
<<DAILY_LOG_TASK_NAME>> | DailyLogRotation | Task name for log rotation |
<<MONTHLY_WINSXS_TASK_NAME>> | MonthlyWinSxSCleanup | Task name for WinSxS optimization |
<<MONTHLY_EVENTLOG_TASK_NAME>> | MonthlyEventLogMaintenance | Task name for event log maintenance |
Step 1: Overview of WinSxS folder optimization
Section titled “Step 1: Overview of WinSxS folder optimization”C:\Windows\WinSxS is the component store that keeps the history of updates and feature components. It tends to grow over long-term operation. You can optimize it using the following DISM commands:
# Analyze the component storeDism /Online /Cleanup-Image /AnalyzeComponentStore
# Clean up superseded componentsDism /Online /Cleanup-Image /StartComponentCleanup
# Completely remove old versions (no rollback possible)Dism /Online /Cleanup-Image /StartComponentCleanup /ResetBaseSince /ResetBase makes it impossible to uninstall older updates, it is safer to run it only at planned times, such as once per month within a maintenance window.
Step 2: Overview of Temp folder cleanup
Section titled “Step 2: Overview of Temp folder cleanup”The directories $env:TEMP and C:\Windows\Temp accumulate temporary files created by installers and applications. Instead of deleting everything blindly, a realistic policy is:
“Delete files older than N days.”
Step 3: Overview of log file rotation
Section titled “Step 3: Overview of log file rotation”Log directories tend to increase with the number of applications, and long-term retention can heavily consume disk space. Since you often need different retention days per directory, it is better to manage them via a PSD1 configuration file.
Step 4: Overview of event log maintenance
Section titled “Step 4: Overview of event log maintenance”Event logs are critical for troubleshooting and auditing, so they should not be cleared frequently without backup. A common approach is to back them up as .evtx and then clear them, for example about once per month.
# Basic pattern for backup and clearwevtutil epl System C:\Logs\System_20250101.evtxwevtutil cl SystemStep 5: Script design policy
Section titled “Step 5: Script design policy”In this article, we prepare four separate scripts, one per purpose:
-
cleanup_temp.ps1
Deletes old files in Temp folders (retention days are parameterized). -
rotate_logs.ps1
Reads a PSD1 configuration file and deletes.logfiles under configured folders. -
optimize_winsxs.ps1
Runs/StartComponentCleanup /ResetBaseon WinSxS and records the reduction amount. -
maintain_eventlogs.ps1
Backs up specified event logs and then clears them.
With this structure, you can run each maintenance task independently from Task Scheduler, and flexibly adjust daily/monthly schedules or policies.
Example directory layout:
C:\└─ Maintenance\ ├─ cleanup_temp.ps1 # Temp folder cleanup (daily) ├─ rotate_logs.ps1 # Log rotation (daily) ├─ optimize_winsxs.ps1 # WinSxS optimization (monthly) ├─ maintain_eventlogs.ps1 # Event log backup + clear (monthly) │ ├─ log_rotation.psd1 # Log rotation config (multiple paths + retention days) │ ├─ Logs\ # Execution logs for each script │ ├─ cleanup_temp_*.log │ ├─ rotate_logs_*.log │ ├─ optimize_winsxs_*.log │ └─ eventlog_maint_*.log │ └─ Backups\ # EVTX backups for event logs ├─ System_YYYYMMDD.evtx ├─ Application_YYYYMMDD.evtx └─ Security_YYYYMMDD.evtxStep 6: Temp cleanup script
Section titled “Step 6: Temp cleanup script”This script recursively deletes files and folders older than DaysToKeep days under the folders specified in $TempPaths, and records the results to a log file.
You can override DaysToKeep and LogPath via parameters.
cleanup_temp.ps1
param( [string[]]$TempPaths = @("$env:TEMP", "C:\Windows\Temp"), [int]$DaysToKeep = 7, [string]$LogPath = "<<LOG_PATH>>")
if (-not (Test-Path $LogPath)) { New-Item $LogPath -ItemType Directory -Force | Out-Null}
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'$logFile = Join-Path $LogPath "cleanup_temp_$timestamp.log"
function Write-Log($Message) { "[{0}] {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message | Tee-Object -FilePath $logFile -Append}
Write-Log "===== Temp cleanup started ====="Write-Log "DaysToKeep = $DaysToKeep"
$limitDate = (Get-Date).AddDays(-$DaysToKeep)
foreach ($path in $TempPaths) { if (-not (Test-Path $path)) { Write-Log "Skip: Not found -> $path" continue }
Write-Log "Processing: $path"
Get-ChildItem -Path $path -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $limitDate } | ForEach-Object { try { Remove-Item -LiteralPath $_.FullName -Force -Recurse -ErrorAction Stop Write-Log "Deleted: $($_.FullName)" } catch { Write-Log "Failed: $($_.FullName) - $($_.Exception.Message)" } }}
Write-Log "===== Temp cleanup finished ====="Step 7: Log rotation script
Section titled “Step 7: Log rotation script”Log rotation configuration file (PSD1)
Section titled “Log rotation configuration file (PSD1)”This PSD1 file defines multiple directory + retention days pairs. To add a new log directory, just add an entry to this file.
log_rotation.psd1
@{ RotationTargets = @( @{ Path = "C:\Logs\App1" DaysToKeep = 7 }, @{ Path = "C:\Logs\App2" DaysToKeep = 30 }, @{ Path = "<<LOG_PATH>>" DaysToKeep = 7 } )}Log rotation script
Section titled “Log rotation script”This script loads RotationTargets from the PSD1 file and, for each path, deletes .log files older than the configured retention. All operations are written to a log file.
rotate_logs.ps1
param( [string]$ConfigPath = "C:\Maintenance\log_rotation.psd1", [string]$LogPath = "<<LOG_PATH>>")
if (-not (Test-Path $LogPath)) { New-Item $LogPath -ItemType Directory -Force | Out-Null}
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'$logFile = Join-Path $LogPath "rotate_logs_$timestamp.log"
function Write-Log($Message) { "[{0}] {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message | Tee-Object -FilePath $logFile -Append}
Write-Log "===== Log rotation started ====="Write-Log "ConfigPath = $ConfigPath"
if (-not (Test-Path $ConfigPath)) { Write-Log "Config file not found. Exit." exit 1}
try { $config = Import-PowerShellDataFile -Path $ConfigPath}catch { Write-Log "Config load failed: $($_.Exception.Message)" exit 1}
foreach ($target in $config.RotationTargets) { $targetPath = $target.Path $daysToKeep = [int]$target.DaysToKeep
if (-not (Test-Path $targetPath)) { Write-Log "Skip (not found): $targetPath" continue }
$limitDate = (Get-Date).AddDays(-$daysToKeep) Write-Log "Path=$targetPath DaysToKeep=$daysToKeep Limit=$limitDate"
Get-ChildItem -Path $targetPath -Recurse -Include *.log -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $limitDate } | ForEach-Object { try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop Write-Log "Deleted: $($_.FullName)" } catch { Write-Log "Failed: $($_.FullName) - $($_.Exception.Message)" } }}
Write-Log "===== Log rotation finished ====="Step 8: WinSxS optimization script
Section titled “Step 8: WinSxS optimization script”This script measures the size of the WinSxS directory before and after running /StartComponentCleanup /ResetBase, and logs the difference. This allows you to see how much disk space was actually reclaimed.
optimize_winsxs.ps1
param( [string]$LogPath = "<<LOG_PATH>>")
if (-not (Test-Path $LogPath)) { New-Item $LogPath -ItemType Directory -Force | Out-Null}
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'$logFile = Join-Path $LogPath "optimize_winsxs_$timestamp.log"
function Write-Log($Message) { "[{0}] {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message | Tee-Object -FilePath $logFile -Append}
Write-Log "===== WinSxS optimization started ====="
$winsxsPath = "C:\Windows\WinSxS"
Write-Log "Measuring WinSxS size before..."$sizeBefore = (Get-ChildItem $winsxsPath -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum$sizeBeforeGB = [math]::Round($sizeBefore / 1GB, 2)Write-Log "Before: $sizeBeforeGB GB"
Dism /Online /Cleanup-Image /AnalyzeComponentStore | Out-File (Join-Path $LogPath "dism_before_$timestamp.txt")
Write-Log "Running StartComponentCleanup /ResetBase..."Dism /Online /Cleanup-Image /StartComponentCleanup /ResetBase | Out-File (Join-Path $LogPath "dism_exec_$timestamp.txt")
Write-Log "Measuring WinSxS size after..."$sizeAfter = (Get-ChildItem $winsxsPath -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum$sizeAfterGB = [math]::Round($sizeAfter / 1GB, 2)
Write-Log "After: $sizeAfterGB GB"Write-Log ("Reduced: {0} GB" -f ([math]::Round(($sizeBefore - $sizeAfter)/1GB,2)))
Dism /Online /Cleanup-Image /AnalyzeComponentStore | Out-File (Join-Path $LogPath "dism_after_$timestamp.txt")
Write-Log "===== WinSxS optimization finished ====="Step 9: Event log maintenance script
Section titled “Step 9: Event log maintenance script”This script, for the specified logs (by default System / Application / Security), exports each one to the backup directory as .evtx and then clears the original log. All results are recorded in a detailed log file.
maintain_eventlogs.ps1
param( [string]$LogPath = "<<LOG_PATH>>", # Destination for .log files [string]$BackupPath = "<<BACKUP_PATH>>", # Destination for EVTX backups [string[]]$EventLogs = @("System", "Application", "Security"))
# Create log directory if missingif (-not (Test-Path $LogPath)) { New-Item $LogPath -ItemType Directory -Force | Out-Null}
# Create backup directory if missingif (-not (Test-Path $BackupPath)) { New-Item $BackupPath -ItemType Directory -Force | Out-Null}
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'$logFile = Join-Path $LogPath "eventlog_maint_$timestamp.log"
function Write-Log($Message) { "[{0}] {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message | Tee-Object -FilePath $logFile -Append}
Write-Log "===== Event log maintenance started ====="Write-Log "BackupPath = $BackupPath"
foreach ($name in $EventLogs) {
# Unique EVTX file per log $destEvtx = Join-Path $BackupPath ("{0}_{1}.evtx" -f $name, $timestamp)
try { Write-Log "Export: $name -> $destEvtx" wevtutil epl $name $destEvtx
Write-Log "Clear: $name" wevtutil cl $name } catch { Write-Log "Failed: $name - $($_.Exception.Message)" }}
Write-Log "===== Event log maintenance finished ====="Step 10: Task Scheduler registration examples
Section titled “Step 10: Task Scheduler registration examples”The following examples show how to register all maintenance scripts using schtasks.exe.
For a detailed explanation of options like /SC, /D, /ST, /RU, and typical patterns, see:
Managing Task Scheduler with schtasks.exe
Temp cleanup (daily task example)
Section titled “Temp cleanup (daily task example)”Runs cleanup_temp.ps1 every day at 02:00, deleting Temp files older than 7 days.
The task runs under <<ADMIN_USER>> with highest privileges (/RL HIGHEST).
schtasks /Create ` /TN "<<DAILY_TEMP_TASK_NAME>>" ` /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Maintenance\cleanup_temp.ps1 -DaysToKeep 7 -LogPath <<LOG_PATH>>" ` /SC DAILY ` /ST 02:00 ` /RU "<<ADMIN_USER>>" ` /RL HIGHEST ` /FLog rotation (daily task example)
Section titled “Log rotation (daily task example)”Runs rotate_logs.ps1 every day at 02:30, rotating .log files across multiple directories according to the PSD1 configuration.
Retention days are controlled only in log_rotation.psd1.
schtasks /Create ` /TN "<<DAILY_LOG_TASK_NAME>>" ` /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Maintenance\rotate_logs.ps1 -ConfigPath C:\Maintenance\log_rotation.psd1 -LogPath <<LOG_PATH>>" ` /SC DAILY ` /ST 02:30 ` /RU "<<ADMIN_USER>>" ` /RL HIGHEST ` /FWinSxS optimization (monthly task example)
Section titled “WinSxS optimization (monthly task example)”Runs optimize_winsxs.ps1 on the 1st of every month at 03:00, executing StartComponentCleanup /ResetBase and measuring size before/after.
Because this includes a non-reversible operation, align the schedule with a maintenance window.
schtasks /Create ` /TN "<<MONTHLY_WINSXS_TASK_NAME>>" ` /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Maintenance\optimize_winsxs.ps1 -LogPath <<LOG_PATH>>" ` /SC MONTHLY ` /D 1 ` /ST 03:00 ` /RU "<<ADMIN_USER>>" ` /RL HIGHEST ` /FEvent log maintenance (monthly task example)
Section titled “Event log maintenance (monthly task example)”Runs maintain_eventlogs.ps1 on the 1st of every month at 03:30, backing up event logs (System / Application / Security, etc.) to the backup directory and then clearing them.
Adjust -EventLogs and -BackupPath to match your audit requirements.
schtasks /Create ` /TN "<<MONTHLY_EVENTLOG_TASK_NAME>>" ` /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Maintenance\maintain_eventlogs.ps1 -LogPath <<LOG_PATH>>" ` /SC MONTHLY ` /D 1 ` /ST 03:30 ` /RU "<<ADMIN_USER>>" ` /RL HIGHEST ` /FSummary
Section titled “Summary”This article showed how to separate Windows Server disk maintenance into purpose-specific scripts, and how to use a PSD1 configuration file to flexibly control log rotation targets and retention.
- Temp cleanup: daily, configurable retention days
- Log rotation: multiple paths + retention days managed via PSD1
- WinSxS optimization: monthly, with logged space reduction
- Event log maintenance: monthly, with EVTX backup before clear
This structure provides a reusable baseline for automated disk maintenance on Windows Server in production environments and can be extended with additional scripts and scheduled tasks as needed.