Disk Full But df Shows Space: Fix Inode Exhaustion



Disclosure: This post contains affiliate links. If you purchase through our links, we may earn a commission at no extra cost to you. We only recommend tools we’ve researched and believe are genuinely useful.

No space left on device — except df -h is staring back at you showing 40GB free. You’ve probably already run it twice, thinking it lied to you the first time. It didn’t. This is disk full but df shows space, and the culprit almost every time is inode exhaustion: your filesystem ran out of the internal bookkeeping slots it uses to track files, not the actual bytes on disk. A directory full of millions of tiny cache files or session logs will do this long before you fill the drive itself. The fix takes about two minutes once you know where to look — df -i, not df -h.

Key Takeaways

  • Inode exhaustion causes ‘No Space Left on Device’ errors even when df shows free disk blocks available.
  • Check inode usage with df -i to identify filesystem saturation before attempting standard disk cleanup procedures.
  • Small files, log entries, and temporary artifacts consume inodes disproportionately and are the primary culprits in exhaustion.
  • Deleting small or temporary files reclaims inodes faster than removing large files when space reporting shows paradoxes.
  • Monitor inode usage proactively with automated cleanup scripts and alerts to prevent future deployment failures and outages.

Quick Fix

Run df -i to confirm inode exhaustion, then follow the steps below to find and clear the directory hoarding them — deletion targets must be verified first. Focus on temporary directories, cache folders, and old log files—these consume the most inodes relative to disk space.

df -i   # confirm: IUse% at 100% = inode exhaustion

Why You See ‘No Space Left on Device’ When df Reports Free Blocks

You’ll see one of two exact messages, depending on what triggered it:

No space left on device
write error: No space left on device

Both come straight from the kernel’s filesystem layer, and both fire under the same condition: something tried to create a new file — a log rotation, a temp cache write, a mail delivery to a spool directory — and the filesystem couldn’t allocate the metadata slot needed to track it. It has nothing to do with how many bytes are free. df -h only reports block usage: the actual storage space consumed by file contents. It never looks at inode consumption, which is why it can show 40GB free while your system is completely unable to write a single new file.

This behavior is consistent across ext4, ext3, btrfs, and XFS, though the internals differ. It shows up most often on systems with millions of small files: log directories that never rotate, PHP session caches, mail spools handling high volume, or CI runners generating throwaway build artifacts by the thousand.

How Inodes and Blocks Work Together

Every file on the filesystem needs two things: an inode, which stores metadata (permissions, timestamps, size, pointers to data location), and one or more blocks, which store the actual file content. You can think of inodes as entries in a card catalog and blocks as the shelf space for books. Run out of catalog cards and it doesn’t matter how much shelf space is left — you can’t check in a new book.

Inode counts are fixed at filesystem creation time, not dynamically expandable afterward on ext4 or XFS. The default ratio — how many bytes of disk space get one inode — varies by mkfs parameters and filesystem type, which is why two drives of identical size can have wildly different inode ceilings.

Check Inode Usage Across All Mounted Filesystems

Before touching anything, confirm this is actually an inode problem and find out which filesystem is the culprit. Run this first:

df -i

This prints inode usage per mounted filesystem instead of block usage. Expect output like this:

Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/sda1      1310720 1310720      0  100% /
/dev/sda2      6553600  89211 6464389    2% /var/log
tmpfs                0      0       0     - /dev/shm

Inodes is the total count available on that filesystem, fixed since creation. IUsed and IFree show what’s consumed and what’s left. IUse% is the number that matters — anything at or near 100% is your problem child. In the example above, /dev/sda1 has zero free inodes even though it’s mounted as root and might have gigabytes of block space untouched.

Now compare against block usage on the same mount:

df -h

If / shows something like 62% used here but 100% in the df -i output, you’ve confirmed inode exhaustion, not disk space exhaustion. That mismatch is the entire diagnosis.

Rows showing 0 in every column — tmpfs, devfs, proc, sysfs — are virtual filesystems that don’t allocate inodes the same way. Ignore those; a dash or zero in IUse% is normal, not an error.

This command works identically on every major Linux distro (Ubuntu, Debian, RHEL, Alpine), on FreeBSD, and on macOS (Ventura through Sequoia), though macOS’s APFS reports inode-equivalent object counts rather than a fixed pre-allocated pool, so near-100% readings are rarer there.

Find Which Directory Is Consuming All Inodes

Knowing a filesystem is out of inodes doesn’t tell you which directory is hoarding them. You need a scan. On a busy server this means walking the tree once and bucketing counts by top-level directory:

find / -xdev -printf '%h\n' 2>/dev/null | cut -d/ -f2 | sort | uniq -c | sort -rn | head -20

This prints the parent directory of every file, tallies how many times each top-level directory shows up, and sorts descending. The -xdev flag keeps find from crossing into other mounted filesystems, so you’re only counting inodes on the one that’s actually full. Expect output like 842103 var sitting far above everything else — that’s your culprit.

On large filesystems (multi-million file counts), this scan can take several minutes and will hammer disk I/O. Run it during a low-traffic window if you can, or expect a slow SSH session while it works.

Once you know the top-level offender, drill down one level at a time:

find /var/log -type f | wc -l

This counts files under a specific directory. Repeat against subdirectories (/var/log/nginx, /var/spool/mail, /var/lib/docker) until you isolate the exact folder with the runaway file count.

If you’re on GNU coreutils 8.32 or newer (Ubuntu 22.04+, Debian 12), du has a shortcut that skips the manual tally:

du --inodes -x /var | sort -rn | head -20

This reports inode usage per directory directly, no piping through cut and sort required.

If the count looks like an active runaway process rather than years of accumulation, narrow by modification time:

find /var/log -type f -mtime -7 | wc -l

This counts only files touched in the last 7 days — useful for catching a logging bug or mail queue backup that’s spiking right now. In practice, three directories cause 90% of these incidents: /var/log (unrotated logs), /tmp (orphaned session files or cache), and /var/spool/mail (undelivered mail queues from a misconfigured MTA).

Delete Small Files or Logs to Reclaim Inodes (Most Common Fix)

Once you’ve found the directory eating your inodes, the fix is almost always deleting a pile of small files. This is the cause about 80% of the time — unrotated logs, dead temp files, or a stuck mail spool. Nothing fancy required, just careful deletion.

Start by checking whether logrotate is even configured on the box:

cat /etc/logrotate.conf

Look for a rotate value and a weekly/daily directive. If this file is missing or the app writing to /var/log isn’t listed under /etc/logrotate.d/, that’s why logs piled up unrotated for months.

Before deleting anything, list what you’re about to remove:

find /var/log -type f -name '*.log' -mtime +30 -print

This prints every log file older than 30 days without touching anything. Scroll through the output — if it’s thousands of rotated .log.1, .log.2 style files, you’re safe to proceed.

Warning: the next command permanently deletes files. There’s no undo, no trash bin, no recovery short of a backup. Double-check the -mtime +30 filter matches what you just reviewed above.

find /var/log -type f -name '*.log' -mtime +30 -delete

This removes every matching log file older than 30 days and returns the freed inodes to the filesystem immediately.

If nginx, apache2, or systemd-journald is actively writing to files in that directory, stop the service first (systemctl stop nginx) so you’re not deleting a file mid-write, which can crash the write and leave a truncated log.

Do the same for orphaned temp files:

find /tmp -type f -atime +7 -delete

This clears anything in /tmp not accessed in 7 days — session files, upload fragments, and cache junk left behind by crashed processes.

Confirm the fix worked:

df -i /

IUse% should have dropped noticeably. This applies to every Linux distro — Ubuntu, Debian, RHEL, Alpine — since inode accounting is a kernel/filesystem feature, not a distro-specific one.

Clear Cache Directories and Temporary Build Artifacts

If log rotation wasn’t the culprit, package caches and build directories are next — these generate huge file counts (npm’s node_modules alone can be 30,000+ tiny files per project). Run these roughly in order of safety, safest first.

apt-get clean

Debian/Ubuntu: clears /var/cache/apt/archives. Fully safe, reversible by re-downloading packages.

yum clean all

RHEL/CentOS/Rocky/Alma: clears yum/dnf metadata and cached RPMs. Same safety profile as apt-get clean.

pacman -Sc

Arch Linux: removes cached packages not currently installed. Prompts before deleting — answer y.

pip cache purge

Python 3.8+: clears pip’s wheel cache under ~/.cache/pip. Safe — pip just re-downloads wheels next install.

npm cache clean --force

Node.js/npm: purges ~/.npm. The --force flag is required on npm 5+ since Node maintainers consider clearing the cache risky enough to gate it — in practice it’s fine.

yarn cache clean

Same idea for Yarn users. If that hangs or errors, delete manually:

find ~/.npm -type f -delete

Blunt but effective when the cache command itself is broken.

rm -rf ~/.cache/*

Warning: this deletes all user-level application caches — browser profiles, thumbnail caches, editor state. Nothing critical, but some apps will rebuild slowly on next launch. Only run this on your own user directory, never as root against another user’s home.

find . -name node_modules -type d -exec rm -rf {} +

Warning: this recursively deletes every node_modules directory under the current path. Run npm install or yarn afterward to rebuild them. A single monorepo can easily hold 500,000+ inodes in nested node_modules — reclaiming these often fixes inode exhaustion outright.

Docker is the other frequent offender:

docker system prune -a

Warning: this deletes all stopped containers, unused networks, dangling images, and any image not referenced by a running container. If you need an image tomorrow, re-pull it. Docker layers are notorious for eating inodes on /var/lib/docker, especially with overlay2.

Remove Duplicate or Orphaned Files from Failed Deployments

If the cache cleanup didn’t recover enough inodes, check /opt and your deploy directories next. Failed CI/CD runs leave behind half-extracted tarballs, old application versions nobody deleted, and backup duplicates from every rsync job that ever ran. This is the third most common cause on Linux servers running applications with a real deployment pipeline — usually 10-15% of cases, but it’s the one people forget to check.

List old files before you touch anything:

find /opt -type f -atime +90 -print

This shows every file under /opt not accessed in 90 days. Read the output carefully — check for config files or license keys that just sit unused but still matter.

find /opt -type f -atime +90 -delete

Warning: this permanently deletes every matched file with no confirmation prompt. If a rarely-accessed config or license file matched the filter, it’s gone. Run the -print version first and review the list.

Next, clear out orphaned Docker build layers — the “none” tagged images left behind by failed multi-stage builds:

docker images -a | grep none | awk '{print $3}' | xargs docker rmi

Warning: this removes all untagged images. Any image still referenced by a running container is protected, but stopped containers built from these images will fail to restart.

If a repo on this box has years of history, git’s reflog and loose objects can also chew through inodes:

cd /repo && git reflog expire --expire=now --all && git gc --prune=now

Warning: this permanently discards reflog entries and unreachable commits. You lose the ability to recover accidentally deleted branches or amended commits.

For ongoing prevention on systemd-based distros (Ubuntu 22.04/24.04, Debian 12, RHEL 9), let the OS handle temp file sprawl automatically:

systemd-tmpfiles --clean

This runs the cleanup rules defined in /etc/tmpfiles.d/ and /usr/lib/tmpfiles.d/, removing aged-out temp files before they accumulate into another inode crisis.

Verify Inode Usage Is Back to Normal

Run sync first — some filesystems (notably ext4 under heavy write load) delay metadata updates, and df can lag behind the actual state for a few seconds.

sync && df -i /opt

Check the IUse% column. Anything under 80% is healthy again; if you’re still above 90%, you haven’t found the real culprit directory yet — repeat the find commands against other high-inode paths like /var/log, /tmp, or /var/lib/docker.

Confirm the filesystem accepts new files, not just that the number looks better:

touch /tmp/test-file-$RANDOM && echo 'Success' && rm /tmp/test-file-*

You should see Success printed with no No space left on device error.

Now restart whatever service was originally failing:

systemctl restart nginx

or, for containerized stacks:

docker-compose up -d

Tail the logs afterward — journalctl -u nginx -n 50 or docker-compose logs -f --tail=50 — and confirm No space left on device no longer appears anywhere in the output.

Prevent Inode Exhaustion: Automatic Cleanup and Monitoring

Cleaning up once fixes today’s outage. It doesn’t stop the same directory from filling back up in three weeks. Applies to any Linux distro — Ubuntu, Debian, RHEL, Alpine — running self-hosted logging, mail queues, or CI job artifacts.

Start with logrotate for anything writing plain-text logs:

cat /etc/logrotate.d/custom
/var/log/myapp/*.log {
    daily
    rotate 7
    maxage 30
    maxsize 50M
    compress
    missingok
    notifempty
}

maxage 30 deletes rotated logs older than 30 days regardless of count; rotate 7 caps how many rotated copies exist at once.

For temp directories and cache files, use systemd-tmpfiles instead of a log-specific tool:

cat /etc/tmpfiles.d/custom.conf
d /var/cache/myapp 0755 root root 7d
d /tmp/uploads 0755 www-data www-data 1d

The Age= field (here 7d and 1d) tells systemd-tmpfiles to delete anything older than that on its next scheduled run.

If you’re on a distro without systemd or just want a blunt fallback, a cron job works fine:

0 2 * * * find /var/log -type f -name '*.log' -mtime +30 -delete

Runs nightly at 2 AM, deleting any .log file untouched for 30+ days.

For monitoring, watch inode usage live during a suspected leak:

watch -n 60 'df -i'

Refreshes every 60 seconds so you can catch a directory climbing in real time.

For unattended alerting, add a cron job that emails when any filesystem crosses 80% inode use:

df -i | awk '$5+0 > 80 {print $0}'

Pipe this into mail -s "inode warning" you@example.com or wire it into an existing alert script.

If you’re already running Prometheus, skip the cron job entirely — node_exporter exposes node_filesystem_files_free per mountpoint. Alert when it drops below 10% of node_filesystem_files and you’ll get paged before df -i ever hits 100%.

Increasing the inode count itself requires reformatting, which is almost never necessary once cleanup is automated:

Warning: this destroys all data on the partition. Unmount first, back up anything you need, then:

mkfs.ext4 -i 4096 /dev/sdX1

Sets one inode per 4096 bytes instead of the ext4 default (roughly one per 16KB), quadrupling your inode ceiling for small-file-heavy workloads like mail spools or session caches.

Frequently Asked Questions

Why does df show free space but I get no space left on device error?

Your filesystem has run out of inodes (file metadata slots), not disk blocks. Each file requires one inode regardless of size. When inodes are exhausted, you cannot create new files even if block space remains. Use df -i to confirm inode exhaustion.

How do I check how many inodes I’m using?

Run df -i to display inode usage across all mounted filesystems. The output shows total inodes, used inodes, available inodes, and percentage used. Filesystems above 90% inode usage are at critical risk of causing ‘No Space Left on Device’ errors.

What’s the fastest way to free up inodes?

Delete small files, log files, and temporary build artifacts in high-inode-consumption directories. Use find to locate and remove files by age or type. Clearing cache directories and temporary deployment artifacts typically reclaims the most inodes quickly.

How can I prevent inode exhaustion in the future?

Implement automated log rotation, cache cleanup scripts, and temporary file purging. Monitor inode usage with cron jobs and set alerts at 80% threshold. Configure applications to limit log file counts and implement retention policies for build artifacts.

Scroll to Top