Disk cleanup and maintenance automation

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

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

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 store
Dism /Online /Cleanup-Image /AnalyzeComponentStore

# Clean up superseded components
Dism /Online /Cleanup-Image /StartComponentCleanup

# Completely remove old versions (no rollback possible)
Dism /Online /Cleanup-Image /StartComponentCleanup /ResetBase

Since /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

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

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

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 clear
wevtutil epl System C:\Logs\System_20250101.evtx
wevtutil cl System

Step 5: Script design policy

In this article, we prepare four separate scripts, one per purpose:

  1. cleanup_temp.ps1
    Deletes old files in Temp folders (retention days are parameterized).

  2. rotate_logs.ps1
    Reads a PSD1 configuration file and deletes .log files under configured folders.

  3. optimize_winsxs.ps1
    Runs /StartComponentCleanup /ResetBase on WinSxS and records the reduction amount.

  4. 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.evtx

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

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

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

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

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 missing
if (-not (Test-Path $LogPath)) {
    New-Item $LogPath -ItemType Directory -Force | Out-Null
}

# Create backup directory if missing
if (-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

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)

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 `
  /F

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 `
  /F

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 `
  /F

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 `
  /F

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.