VPS Out of Inodes: Fix 'No Space Left on Device' Errors
You SSH in, run df -h, and the disk shows 40% free — but every write still fails with "No space left on device." That's not a disk space problem. It's an inode problem, and it needs a completely different fix than freeing up gigabytes.
Symptom: plenty of space, but nothing will write
This one confuses people because every instinct says "check disk usage first," and disk usage looks fine. What you'll actually see:
- PHP, MySQL, or your app logs errors like
No space left on deviceorENOSPC - You can't create new files, but editing existing ones works fine
apt installoryum updatefails partway through extracting packages- WordPress media uploads or session files fail with vague "unable to write" errors
df -hshows 30-60% used — nowhere close to full
That last point is the tell. A genuinely full disk and an exhausted inode table produce the exact same symptom — a failed write — but df -h only tracks one of them.
Cause: an inode table is a fixed count, not a size limit
Every file and directory on a Linux filesystem, no matter how small, consumes exactly one inode — a metadata slot that stores ownership, permissions, and a pointer to the file's data blocks. On ext4 (the default on almost every VPS image), the total number of inodes is decided once, at mkfs time, based on an assumed average file size. It does not grow automatically as you use the disk.
So a 20GB volume formatted with a lot of small files in mind might have 1.3 million inodes. If something on that server starts creating millions of tiny files — session caches, mail queue files, a runaway logger, a poorly-configured cron job — you can burn through every inode while 60% of the actual bytes sit empty. Once the inode table is full, the kernel refuses new files even though there's plenty of physical space left to store them.
Common culprits we see on Getwebup VPS instances:
| Culprit | Typical location | Why it explodes |
|---|---|---|
| PHP session files | /var/lib/php/sessions | Default file-based sessions never expire cleanly under load |
| Mail queue | /var/spool/exim or /var/spool/postfix | A compromised script or contact form spamming outbound mail |
| npm / node_modules | ~/app/node_modules | Nested dependency trees can hit tens of thousands of tiny files per project |
| Application cache | wp-content/cache, framework tmp dirs | Object cache or page cache writing one file per request, never cleared |
| Docker layers | /var/lib/docker/overlay2 | Each image layer and container adds thousands of small files |
| Log rotation not running | /var/log | Rotated logs pile up as separate files instead of being compressed and pruned |
Fix: confirm it, find it, clear it
Step 1 — confirm it's actually inodes
Run both checks side by side — this is the only way to tell the two problems apart:
df -h # disk space by partition
df -i # inode usage by partition
If df -h shows comfortable free space but df -i shows IUse% at 95-100% on the same partition, you've confirmed it.
Step 2 — find where the inodes went
Don't guess — walk the tree from the root down and count files per directory, drilling into whichever one is heaviest:
for d in /*/; do
echo -n "$d: "
find "$d" -xdev | wc -l
done | sort -t: -k2 -n -r
Once you've identified the heavy top-level directory (usually /var or /home), repeat one level deeper inside it until you land on the actual folder responsible. It's almost always one of the culprits in the table above, not a spread across the whole disk.
Step 3 — clear it immediately
Once you've located the offender, clear it with the tool built for that job rather than a blind rm -rf:
# Stale PHP sessions older than 24 hours
find /var/lib/php/sessions -type f -mtime +1 -delete
# Exim queue — count first, then flush if it's a compromise
exim -bpc
exim -qff
# Docker dangling layers and stopped containers
docker system prune -a --volumes
# npm cache and unused node_modules in old deploys
npm cache clean --force
find / -xdev -name "node_modules" -type d -prune -mtime +30 -exec rm -rf {} \;
Check df -i again after each step — you want to see IUse% actually drop before moving to the next directory.
Step 4 — if it keeps happening, the volume is genuinely undersized
This is the part people get wrong: resizing the disk with resize2fs after a VPS storage upgrade grows the available space, but it does not add new inodes to an existing ext4 filesystem — the inode table was allocated at format time and stays fixed. If your workload is inherently file-count-heavy (large session volume, a queue-heavy app, lots of small cached assets), you have two real options:
- Reformat with a higher inode ratio. When you rebuild the filesystem, pass
mkfs.ext4 -N <inode-count>or a smaller-ibytes-per-inode value to allocate more inodes up front. This requires backing up, reformatting, and restoring — plan it as a maintenance window. - Move to XFS for that volume. XFS allocates inodes dynamically as needed instead of fixing the count at format time, which sidesteps this entire class of problem for workloads that generate huge numbers of small files.
Either way, don't just attach more disk space and assume it's fixed — check df -i on the new volume before you call it done.
Prevention: stop landing back here
- Move PHP sessions to Redis or Memcached instead of the filesystem if your app creates a high volume of them — this removes the biggest recurring offender entirely.
- Add
df -ito your monitoring alongsidedf -h— a disk-space alert alone will miss this failure mode completely. - Confirm logrotate actually runs (
logrotate -d /etc/logrotate.conffor a dry run) rather than assuming the cron job is healthy. - Set a retention policy on Docker images and cache directories instead of letting them grow unbounded between deploys.
- If you provision new volumes, decide the expected average file size upfront and format accordingly — it's much cheaper to get the inode ratio right at
mkfstime than to migrate later.
A full inode table is completely recoverable once you know to check for it — the trap is spending an hour staring at df -h and a clean disk, wondering why nothing will write.
Frequently asked questions
How is inode exhaustion different from a full disk?
A full disk means you've used up the actual storage bytes on the volume. Inode exhaustion means you've used up the fixed number of file-metadata slots the filesystem allocated at format time — you can hit this with plenty of free space left if you have a huge number of very small files.
Will resizing my VPS disk fix inode exhaustion?
No. Growing an existing ext4 filesystem with resize2fs adds space but doesn't add inodes to the original filesystem's inode table. You need to reformat with a higher inode count (or a different filesystem like XFS) to actually add more inodes.
How do I check inode usage on my VPS?
Run df -i alongside df -h. df -h shows byte usage; df -i shows the percentage of inodes used per mounted partition. If df -i is near 100% while df -h looks fine, you're dealing with inode exhaustion, not disk space.
What usually causes inode exhaustion on a hosting VPS?
The most common causes are file-based PHP session storage, a runaway or compromised mail queue, unpruned Docker image layers, deep node_modules trees, and application caches that write one file per request without ever clearing old ones.
Can I switch an existing ext4 volume to XFS without losing data?
Not in place. You'd need to back up the data, reformat the volume as XFS (or a new ext4 filesystem with a higher inode allocation), and restore from backup. Treat it as a planned maintenance window, not a live change.