One day your Windows machine is fine. The next, File Explorer shows a red bar and a system drive with almost no room left. If you run WSL2 (the Windows Subsystem for Linux, which lets you run real Ubuntu inside Windows) and Docker, there is a common hidden cause: crash dump files that quietly pile up in your temp folder.
This guide shows you how to free up C: drive space on WSL setups by finding what is actually eating it, cleaning the safe junk without touching real data, and setting up a small script so it never sneaks up on you again. The examples are run on WSL2 Ubuntu in Windows, but the cleanup logic works the same on any Windows 10 or 11 machine.
Prerequisites
- A Windows 10 or 11 machine with WSL2 installed
- Basic comfort running commands in PowerShell
- Docker Desktop (optional, only if you also want to clean Docker images)
- If WSL itself is throwing startup errors on top of the disk problem, fix that first with How to Fix WSL CreateVm/HCS/ERROR_FILE_NOT_FOUND on Windows
Step-by-Step Guide
Step 1: Measure before you delete
Never start deleting blindly. The smart move is to size your top-level folders first, then drill into the biggest one. Think of it like checking which room in the house is full before you start throwing things out. Open PowerShell and run:
Get-ChildItem C:\ -Directory -Force -ErrorAction SilentlyContinue | ForEach-Object {
$size = (Get-ChildItem $_.FullName -Recurse -File -Force -ErrorAction SilentlyContinue |
Measure-Object Length -Sum).Sum
[PSCustomObject]@{ Folder = $_.FullName; SizeGB = [math]::Round($size/1GB, 2) }
} | Sort-Object SizeGB -Descending | Select-Object -First 10
Get-ChildItem C:\ -Directory: lists the folders at the root of your C: driveMeasure-Object Length -Sum: adds up the size of every file inside each folderSort-Object SizeGB -Descending: puts the biggest folders at the top
On the machine in this example, the result pointed straight at the user profile:
| Folder | Size |
|---|---|
| C:\Users | 279 GB |
| C:\Windows | 57 GB |
| C:\Program Files | 14 GB |
C:\Users was the whale. Following the trail into
AppData\Local showed two surprises: a Packages folder
at 110 GB (that is your WSL Linux disk) and a Temp folder at 90 GB.
A 90 GB temp folder is not normal. That is usually the sign that something is
crashing in a loop.
Step 2: Find the real culprit in %TEMP%
Break down what is inside the temp folder the same way:
Get-ChildItem $env:TEMP -Force | ForEach-Object {
$size = if ($_.PSIsContainer) {
(Get-ChildItem $_.FullName -Recurse -File -Force -ErrorAction SilentlyContinue |
Measure-Object Length -Sum).Sum
} else { $_.Length }
[PSCustomObject]@{ Name = $_.Name; SizeGB = [math]::Round($size/1GB, 2) }
} | Sort-Object SizeGB -Descending | Select-Object -First 5
One folder stood out: wsl-crashes at 77 GB. Inside were sixteen
crash dump files, each around 10 GB, with names like
wsl-crash-*-_usr_bin_node-6.dmp. WSL had been crashing a Node.js
process and writing a 10 GB core dump (a snapshot of memory at the moment of the
crash) every single time. Nobody told Windows to delete them, so they just kept
stacking up.
Here is the important part: the full disk was a symptom, not the real bug. Something was crashing. Deleting the dumps frees space today, but until the crash itself is fixed they will come back. If the crashing process is tied to a specific Node version, switching it can help, see How to Switch Node.js Version in WSL Ubuntu. Cleanup buys you breathing room to chase the root cause.
Step 3: Clean the safe stuff
Crash dumps are pure garbage, so they are safe to delete. Old temp files and the Recycle Bin are safe too. What you do not delete is the WSL Linux disk or your Docker data, those are real files you still use. Clear the dumps and empty the Recycle Bin:
Get-ChildItem "$env:TEMP\wsl-crashes" -File -Force | Remove-Item -Force
Clear-RecycleBin -Force
Get-ChildItem ... | Remove-Item -Force: deletes the crash dump filesClear-RecycleBin -Force: empties the Recycle Bin without a prompt
On the example machine this took free space from 34.5 GB back up to about 123 GB, roughly 89 GB recovered, with no real data touched.
Step 4: Clean Docker (optional)
Docker tends to hoard unused images and build cache. First, see what can be reclaimed (this changes nothing):
docker system df
Then prune. Here are the common options, from safe to aggressive:
| Command | What it removes |
|---|---|
docker system prune -f | Stopped containers, dangling images, unused networks, build cache |
docker system prune -a -f | Everything above, plus all unused images (you re-pull later) |
docker builder prune -f | Build cache only |
A word of caution: never add --volumes unless you are sure, since
that deletes data stored in Docker volumes (like local databases). If you only
want to clear the leftover untagged images, the focused approach in
How to Remove All None Tag on Docker Images
is a gentler option. Docker Desktop must be running for any prune command to work.
Step 5: Automate it with a reusable script
A one-time cleanup is a chore you will forget. A small script fixes that. The one
below is built to be careful: it can run in preview mode (it shows what it would
delete without deleting anything), it logs every action, and it never touches real
data. Save it as Clean-CDrive.ps1.
<#
.SYNOPSIS
Safe C: drive cleanup for recurring WSL crash dumps and temp bloat.
.DESCRIPTION
Deletes known-safe junk: WSL crash dumps, old temp files, Recycle Bin,
and optionally prunes Docker. Logs everything. Never touches WSL/Docker
virtual disks, your files, or running files.
.PARAMETER WhatIf
Dry run. Shows what WOULD be deleted without deleting anything.
.PARAMETER TempOlderThanDays
Only delete %TEMP% items older than this many days (default 7).
.PARAMETER MinFreeGB
Only run cleanup when free space is BELOW this value.
.PARAMETER SkipDocker
Do not touch Docker at all.
.PARAMETER DockerAll
Aggressive Docker prune (removes ALL unused images, not just dangling).
#>
[CmdletBinding()]
param(
[switch]$WhatIf,
[int]$TempOlderThanDays = 7,
[double]$MinFreeGB = 0,
[switch]$SkipDocker,
[switch]$DockerAll
)
$ErrorActionPreference = 'SilentlyContinue'
$logFile = Join-Path $PSScriptRoot 'Clean-CDrive.log'
function Write-Log {
param([string]$Msg)
$line = ('{0} {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Msg)
Write-Host $line
Add-Content -Path $logFile -Value $line
}
function Get-FreeGB { [math]::Round((Get-PSDrive C).Free / 1GB, 1) }
Write-Log '=========================================='
Write-Log ('Clean-CDrive start (WhatIf={0})' -f $WhatIf.IsPresent)
$freeBefore = Get-FreeGB
Write-Log ('Free space before: {0} GB' -f $freeBefore)
if ($MinFreeGB -gt 0 -and $freeBefore -ge $MinFreeGB) {
Write-Log ('Free {0} GB is at or above threshold {1} GB. Nothing to do.' -f $freeBefore, $MinFreeGB)
return
}
$freedTotal = 0
# 1. WSL crash dumps
$wslCrash = Join-Path $env:TEMP 'wsl-crashes'
if (Test-Path $wslCrash) {
foreach ($f in (Get-ChildItem $wslCrash -File -Force)) {
if ($WhatIf) {
Write-Log (' WOULD DELETE: {0} ({1} GB)' -f $f.Name, [math]::Round($f.Length/1GB,2))
} else {
$b = $f.Length; Remove-Item $f.FullName -Force
if (-not (Test-Path $f.FullName)) { $freedTotal += $b }
}
}
}
# 2. Old temp items
$cutoff = (Get-Date).AddDays(-$TempOlderThanDays)
$skip = @('claude','wsl-crashes')
foreach ($i in (Get-ChildItem $env:TEMP -Force |
Where-Object { $skip -notcontains $_.Name -and $_.LastWriteTime -lt $cutoff })) {
$sz = if ($i.PSIsContainer) {
(Get-ChildItem $i.FullName -Recurse -File -Force | Measure-Object Length -Sum).Sum
} else { $i.Length }
if ($null -eq $sz) { $sz = 0 }
if ($WhatIf) {
Write-Log (' WOULD DELETE: {0} ({1} GB)' -f $i.Name, [math]::Round($sz/1GB,2))
} else {
Remove-Item $i.FullName -Recurse -Force
if (-not (Test-Path $i.FullName)) { $freedTotal += $sz }
}
}
# 3. Recycle Bin
if ($WhatIf) { Write-Log '[recyclebin] WOULD empty' }
else { try { Clear-RecycleBin -Force -ErrorAction Stop; Write-Log '[recyclebin] emptied' }
catch { Write-Log ('[recyclebin] {0}' -f $_.Exception.Message) } }
# 4. Docker prune (never --volumes; that would delete data)
if ($SkipDocker) { Write-Log '[docker] skipped' }
elseif (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
Write-Log '[docker] not installed. skipped'
} else {
$ver = & docker version --format '{{.Server.Version}}' 2>$null
if ($LASTEXITCODE -ne 0 -or -not $ver) {
Write-Log '[docker] daemon not running. skipped'
} elseif ($WhatIf) {
Write-Log '[docker] reclaimable (docker system df):'
(& docker system df 2>$null) | ForEach-Object { Write-Log (' ' + $_) }
Write-Log (' WOULD PRUNE: docker system prune -f{0}' -f $(if($DockerAll){' -a'}else{''}))
} else {
$a = @('system','prune','-f'); if ($DockerAll) { $a += '-a' }
Write-Log ('[docker] running: docker {0}' -f ($a -join ' '))
(& docker @a 2>&1) | ForEach-Object { Write-Log (' ' + $_) }
}
}
$freeAfter = Get-FreeGB
if ($WhatIf) { Write-Log ('DRY RUN complete. Free: {0} GB (no changes)' -f $freeAfter) }
else { Write-Log ('Freed ~{0} GB. Free: {1} -> {2} GB' -f [math]::Round($freedTotal/1GB,2), $freeBefore, $freeAfter) }
Write-Log 'Clean-CDrive end'
$global:LASTEXITCODE = 0
exit 0
To preview first (nothing gets deleted), then run for real:
.\Clean-CDrive.ps1 -WhatIf
.\Clean-CDrive.ps1
-WhatIf: preview mode, lists every “WOULD DELETE” line but changes nothing- No flag: the real run, it deletes and prints how much space it freed
-DockerAll: also clears all unused Docker images-MinFreeGB 50: only act when free space drops below 50 GB
Step 6: Schedule a weekly preview
The safest habit is a weekly report that deletes nothing. It runs in the
background, writes to the log, and lets you decide when an actual cleanup is worth
it. This registers a Windows scheduled task that runs every Monday at 9 AM in
preview mode. Replace YourName with your Windows username:
$script = "C:\Users\YourName\Scripts\Clean-CDrive.ps1"
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-NoExit -NoProfile -ExecutionPolicy Bypass -File `"$script`" -WhatIf"
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 9:00AM
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable
Register-ScheduledTask -TaskName "CDrive-WeeklyPreview" `
-Action $action -Trigger $trigger -Settings $settings -Force
New-ScheduledTaskAction: defines what runs (PowerShell with the script in preview mode)-NoExit: keeps the window open after it runs so you can read the reportNew-ScheduledTaskTrigger -Weekly: sets it to run every Monday morning-StartWhenAvailable: runs late if the PC was off at the scheduled time
When the weekly report shows the dumps creeping back, you run the real cleanup by hand. That keeps a machine that quietly deletes files out of the picture, you stay in control. If high disk use is paired with sluggish performance, it is often the same underlying cause as VSCode WSL2 high memory usage from AI coding extensions, worth checking while you are at it.
Conclusion
A full C: drive is almost never random. By measuring first, you found 77 GB of WSL crash dumps, cleaned them and other safe junk to recover about 89 GB, and put a weekly preview in place so it never surprises you again. The cleanest cleanup is one you rarely need to run, so the real next step is fixing why WSL was crashing. Start with How to Fix WSL CreateVm/HCS/ERROR_FILE_NOT_FOUND on Windows if WSL is also unstable, or tune your setup with How to Switch Node.js Version in WSL Ubuntu if a Node process is the one crashing.


