No space left on device in GitHub Actions
"No space left on device" in GitHub Actions means the runner disk filled up mid-job and the next write failed. Find out which of the three shapes you have, bytes, inodes or watchers, then reclaim space before the step that needs it rather than after.


What this error means
A step fails the moment it tries to write something, and the wording depends on which tool got there first. tar: write error: No space left on device during a cache save, the Node form, "Error: ENOSPC: no space left on device, write", OSError: [Errno 28] No space left on device from Python, failed to register layer followed by the same phrase from a Docker pull. The step before it usually succeeded, because the disk crossed the line between the two. A confusing variant reports the same errno with plenty of free space in df -h: that one is inodes or inotify watchers, not bytes. The run below is on a Latchkey runner with a 96 GB disk rather than a hosted runner's 14 GB; a full filesystem behaves the same way at either size, and the only number that changes is how long it takes to fill.
/dev/root 96G 96G 600M 100% /
dd: error writing '/home/runner/build-artifact.tar': No space left on device
600+0 records in
599+0 records out
628097024 bytes (628 MB, 599 MiB) copied, 3.95684 s, 159 MB/sReproduced on a Latchkey runner
/dev/root 96G 96G 600M 100% /
dd: error writing '/home/runner/build-artifact.tar': No space left on device
600+0 records in
599+0 records out
628097024 bytes (628 MB, 599 MiB) copied, 3.95684 s, 159 MB/s
[latchkey-bash-wrapper] BEGIN sidecar POST (boot_wait=30s max_time=320s url=http://localhost/diagnose socket=/run/latchkey-self-heal/sock)
[latchkey-bash-wrapper] END sidecar POST ok (attempts=1 http=200)
/dev/root 96G 54G 42G 57% /
packaged 1.2G artifact
1200+0 records in
1200+0 records out
1258291200 bytes (1.3 GB, 1.2 GiB) copied, 0.6136 s, 2.1 GB/sFreed disk space via docker/npm/pip/tmp/gradle/cargo/terraform cache cleanup and retried after ENOSPC
Bytes, inodes or watchers: two commands tell you which
ENOSPC is errno 28, and the kernel raises it for more than one reason. Running both forms of df before you change anything saves you from fixing the wrong thing.
df -h at 100 per cent is the ordinary case: the filesystem is out of blocks. df -i at 100 per cent with free space in df -h is inode exhaustion, which comes from millions of small files rather than large ones, and no amount of deleting big artifacts fixes it. A message containing "System limit for number of file watchers reached" is neither: that is the inotify limit, it has nothing to do with the disk, and it means a watch-mode command is running in CI where it should not be.
df -h /
df -i /
sudo du -h -d1 /home/runner /var/lib/docker /tmp | sort -h | tail -15 || trueCommon causes
The job writes more than the runner holds
The ordinary case. A container build, a dataset, a browser download and a packed artifact are each fine on their own and do not fit together in 14 GB. The step that fails is rarely the greedy one; it is whichever step happened to need the last megabyte.
Docker layers and build cache accumulate through the job
Every pulled image, every intermediate layer and every BuildKit cache entry stays on the disk until something removes it. A multi-stage build that discards its build stage still wrote that stage to disk first, so the peak matters more than the final image size.
The caches you restore land on the same disk as the work
A restored npm, pip or Gradle cache is spent before the job starts, and a cache save at the end needs room for the archive as well as the originals. In our experience a job that starts failing after a cache key change is usually restoring a much larger cache than the one it used to.
It is not bytes at all
Inode exhaustion produces the same errno with a half-empty disk, and so does the inotify watch limit, which reports "System limit for number of file watchers reached". The first comes from millions of tiny files, the second from running a watch-mode command in CI. Neither is fixed by deleting large files.
How to fix it
Measure before you delete
- Run
df -h /anddf -i /in the failing job to separate blocks from inodes. - List the biggest consumers with
du, so you reclaim the directory that matters rather than the one you remember. - Add the same two commands to a failure step permanently: the next occurrence then arrives with its own evidence.
- name: Disk state
if: failure()
run: |
df -h /
df -i /
sudo du -h -d1 /home/runner /var/lib/docker | sort -h | tail || trueReclaim space before the heavy step, not after it fails
Cleanup belongs early in the job, where it is cheap and deterministic. Removing the preinstalled toolchains a job does not use is the single biggest win on a hosted runner, and pruning Docker is the biggest one on a reused runner.
- name: Free disk before the build
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/hostedtoolcache
sudo apt-get clean
docker system prune -af --volumes
df -h /Write less per job
Scope the caches to what the job uses, stop uploading artifacts nothing downloads, and stream large downloads into the process that consumes them instead of landing them on disk first. A job that never writes the file never needs the space.
- uses: actions/cache@v6
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
# not: path: node_modules, which is larger and rebuilt anywayFix the watcher limit rather than the disk
If the message names file watchers, the disk is a red herring. Run the build command rather than the watch command in CI, and raise the inotify limit only when something genuinely has to watch.
sudo sysctl fs.inotify.max_user_watches=524288Where the space on a hosted runner goes
A standard GitHub-hosted runner gives the job 14 GB of SSD, and the image arrives with a large share of it already spent on preinstalled toolchains you are probably not using. That is the headroom every other number on this page competes for.
On top of that, a job adds its checkout, its dependency install, everything the caches restore, every Docker layer it pulls, and every artifact it packs before upload. A cache save is particularly easy to underestimate: tar writes the archive to the same disk before it uploads it, so a 5 GB cache needs 5 GB free on top of the 5 GB it is archiving.
Freeing space is a routine part of a Docker-heavy job rather than an emergency measure. Freeing disk space on GitHub Actions runners has the measured version: which directories are worth removing, and how much each one gave back on a real runner.
If /tmp or /dev/shm is the thing that filled
Not every full filesystem is the root one. Compilers, linkers and package managers write intermediates to TMPDIR, and browsers write shared-memory segments to /dev/shm, which on a container defaults to 64 MB. A Chromium test suite that crashes with "Target closed" after a /dev/shm write error is hitting that limit and not the disk.
Point TMPDIR at the roomiest filesystem you have for the step that needs it, and give containers a larger /dev/shm or run the browser with the flag that bypasses it.
env:
TMPDIR: ${{ github.workspace }}/tmp
# docker
docker run --shm-size=1g my-image
# chromium
chrome --disable-dev-shm-usageWhat the runner does about it, and what it cannot do
Latchkey's pattern for this failure, ENOSPC_DISK_FULL, is the highest-confidence entry in the memory and disk set at 0.99, because the wording is unambiguous: its recorded false-positive risk is only a test assertion that prints the literal string, and its cleanup "is idempotent and will simply do nothing if disk is fine".
The inode variant is treated differently and the difference is worth knowing. The engine carries a separate INODE_EXHAUSTED pattern for a create or mkdir that fails with the same errno, and that pattern is in shadow mode: it observes and reports, and it does not yet repair automatically. An inode-exhausted job on any runner still needs you to delete the file-heavy directory yourself.
How to prevent it
- Put a cleanup step at the top of any Docker-heavy or dataset-heavy job.
- Cache the package manager's store, not the installed tree.
- Record
df -handdf -ion failure so the next occurrence is diagnosed in one read. - Right-size the runner when a job needs the space every time: cleanup is a workaround for a machine that is too small.
Frequently asked questions
How much disk space does a GitHub Actions runner have?
Why does df show free space but writes still fail?
df -i to confirm, then delete the directory with the file count rather than the one with the size.