How to free disk space on GitHub Actions runners
To free disk space on a GitHub Actions runner, remove the preinstalled toolchains your job does not use: .NET, the Android SDK and the hosted tool cache were 18.5 GB between them on the 96 GB image we measured. A hosted runner has 14 GB in total, so the share is what matters, and one step at the top of the job reclaims it.

Most advice on this problem is a copied block of rm -rf lines with no numbers attached, so nobody knows which lines are earning their keep and which are cargo. This page has the numbers from one runner, measured the same day it was written, and it puts the steps in the order that pays.
It is the companion to no space left on device in GitHub Actions, which covers the failure itself. If your job is failing right now, start there; if you want it to stop failing, start here.
Measure first, in the job that is failing
Runner images change. The directory that was worth 11 GB last quarter may not exist this quarter, and a cleanup block that deletes paths which are already gone reports success while freeing nothing. Spend one run finding out what is actually on the machine.
Three commands answer it. df -h / gives the headroom, du ranked by size names the consumers, and docker system df separates images from build cache, which prune differently.
df -h /
sudo du -sh /usr/share/dotnet /usr/local/lib/android /opt/hostedtoolcache \
/usr/share/swift /usr/local/.ghcup /usr/lib/jvm /usr/share/miniconda || true
docker system dfWhat each directory gave back
These are the figures from one pass on a Latchkey latchkey-small runner on 2026-09-19. The image is not a GitHub-hosted image and its disk is much larger, so read the sizes rather than the free-space totals: the same toolchains ship on the GitHub image, where they compete for 14 GB instead of 96 GB.
| Directory | Size on the image | Free space after removing it |
|---|---|---|
| (starting point) | 42.5 GB free of 96 GB | |
docker system prune -af --volumes | 0 B reclaimable | 42.5 GB, unchanged |
/usr/share/dotnet | 5.5 GB | 48.1 GB |
/usr/local/lib/android | 10.6 GB | 58.7 GB |
/opt/hostedtoolcache | 2.4 GB | 61.1 GB |
sudo apt-get clean | the rest | 62 GB |
The order that pays
Remove the large toolchains first. On the image above, .NET, the Android SDK and the hosted tool cache were 18.5 GB between them, and no job that is not building .NET, Android or a pinned tool version needs any of it. Deleting them costs a few seconds.
Prune Docker second, and only if the job pulls or builds images. On a fresh hosted runner it reclaims nothing; on a self-hosted or reused runner it is usually the largest single win, because layers and build cache accumulate across every job the machine has run.
Clean the apt cache third. It is small and free.
Stop there. Each additional path in a cleanup block is another thing that can silently no-op, and the long rm -rf lists that circulate are mostly the same three directories plus paths that no longer exist.
- name: Free disk space
run: |
df -h /
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/hostedtoolcache
sudo apt-get clean
docker system prune -af --volumes
df -h /Use the action, or write the four lines
The community action jlumbroso/free-disk-space does the same job with switches per category, and it is a reasonable choice when you want someone else to track which paths still exist on the current image. It is slower than the four lines above, because by default it removes a great deal more.
Whichever you use, print df -h before and after. A cleanup step that reports nothing is indistinguishable from a cleanup step that freed nothing, and the second one happens more often than people think.
What not to delete
Leave the runner's own work directory, /home/runner/work, alone: that is your checkout. Leave /home/runner/runners alone: that is the agent running the job. Leave the language runtime your job uses alone, which sounds obvious until a cleanup block copied from a Node repository lands in a Python one and removes the toolcache the setup step just populated.
Ordering matters for that last one. Cleanup belongs before the setup steps that install tools, not after, or you will delete the thing you just spent thirty seconds installing.
When freeing space is the wrong fix
Cleanup is a workaround for a machine that is too small. If a job needs the space on every run, the durable answer is a larger disk rather than a ritual at the top of every workflow, and the ritual has a cost: it runs on every job, it fails silently when an image changes, and it is one more thing to maintain.
A managed runner sized for the work removes the step entirely. On a Latchkey runner the disk-full failure is also repaired on the runner: the recorded run in no space left on device in GitHub Actions shows the engine reclaiming space mid-job and the retried step writing the artifact it could not write a minute earlier.
Key takeaways
- Measure with
df -handdubefore copying anyone's cleanup block, including this one. - Three directories carry most of the win: .NET, the Android SDK and the hosted tool cache.
- Docker prune is the biggest lever on a reused runner and reclaims nothing on a fresh one.
- Put the cleanup before the setup steps, never after.
Frequently asked questions
How do I increase disk space on a GitHub Actions runner?
Is it safe to delete /usr/share/dotnet and the Android SDK?
Should I use the free-disk-space action instead?
df -h before and after so you can see what it actually did.