Skip to main content
RunBook Academy

← All runbooks in Linux

medium riskservice affecting~30 min

Runbook: Filesystem full - blocks, inodes, and deleted-but-open files

1 · Prerequisites

Confirm every item is in place before any state change.

2 · Pre-checks

Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.

  • · Identify the filesystem that is actually full with df -h and df -i - not the directory someone mentioned
  • · Confirm whether it is blocks or inodes; they have different fixes
  • · Confirm whether the host is a cluster node before restarting anything
  • · Confirm what the filesystem holds: logs, database, application data, or root
  • · Note the current free space so you can prove the reclaim worked

3 · Procedure

Execute each step in order. Verify the expected output of a step before moving to the next.

  1. 1df -h and df -i on every mount, to find the real culprit
  2. 2Check for deleted-but-open files with lsof +L1 before deleting anything
  3. 3If inodes are exhausted, find the directory with the file count, not the byte count
  4. 4If blocks are exhausted, find the largest consumers with du, bounded to one filesystem
  5. 5Reclaim in the safe order: rotate and vacuum logs, clear caches, then archive
  6. 6Restart only the specific process holding deleted files, if that was the cause
  7. 7Verify free space and free inodes have actually recovered
  8. 8Fix the cause: rotation policy, retention, quota, or monitoring threshold

4 · Verification

Confirm the procedure actually fixed the problem.

  • df -h shows the mount below its alert threshold
  • df -i shows inode use below its alert threshold
  • lsof +L1 on that filesystem returns nothing significant
  • The affected service writes successfully again (a real write test, not just is-active)
  • The monitoring alert has cleared on its own, not been silenced

5 · Rollback

If verification fails, undo the procedure in reverse order.

  • Restore any file deleted in error from backup or snapshot - list what you deleted before you delete it
  • If a log rotation change caused a service to lose its log handle, restart that service
  • If a filesystem was grown, an LVM/filesystem grow is not reversible; roll back at the volume-manager level only from a snapshot

6 · Escalation

When the runbook isn't enough, contact:

  • · The full filesystem is a database volume: escalate to the data team before deleting anything
  • · Space is consumed by a process you cannot identify or stop: escalate to the application owner
  • · The filesystem is full again within minutes of reclaiming: escalate; you have a runaway writer, not a capacity problem
  • · Root filesystem full on a cluster node with resources running: escalate to the cluster on-call before restarting services

“Disk full” is three different incidents wearing the same alert. Blocks exhausted, inodes exhausted, and space held by deleted-but-still-open files each look like No space left on device and each has a different fix. Getting the classification wrong is how an operator spends twenty minutes deleting files that free nothing.

Step 1: Blocks or inodes?

Run both. Always both.

Read-only / Safedf
df -h
df -i

# Local filesystems only - skip NFS, which can hang when the server is down
df -hlT -x tmpfs -x devtmpfs
  • df -h at 100% and df -i low → block exhaustion. Something large. Go to Step 3.
  • df -h with free space but df -i at 100% → inode exhaustion. Something numerous. Go to Step 4.
  • Both look fine but writes still fail → deleted-but-open files, or you are looking at the wrong mount. Go to Step 2.

Step 2: Deleted-but-open files

Check this before you delete anything. It is the fastest fix in this runbook and the one most often missed.

Read-only / Safelsof +L1
# Files with link count < 1: unlinked but still held open
sudo lsof +L1

# Narrow to the affected filesystem and sort by size
sudo lsof +L1 /var | awk 'NR==1 || $7 > 100000000'

# The gap that confirms it
sudo du -sxh /var
df -h /var

The typical cause is a log file rotated or rm-ed while the daemon still holds it open. The daemon keeps writing to an inode with no name, and the space is never returned until that file descriptor closes.

Service impact possiblereclaim held space
# Preferred: tell the daemon to reopen its logs (no restart, no lost data)
sudo systemctl reload rsyslog
sudo systemctl kill -s HUP nginx.service

# If the daemon has no reopen path, truncate the descriptor in place.
# This frees the blocks immediately and keeps the process running.
# 1234 is the PID, 5 is the fd number, both from the lsof output.
sudo truncate -s 0 /proc/1234/fd/5

# Last resort: restart the process holding it
sudo systemctl restart <unit>

Step 3: Block exhaustion - find the size

Read-only / Safedu -x
# Top-level offenders, bounded to this filesystem
sudo du -xh --max-depth=1 / 2>/dev/null | sort -rh | head -20

# Then descend into the winner
sudo du -xh --max-depth=1 /var 2>/dev/null | sort -rh | head -20

# The 20 largest individual files on this filesystem
sudo find / -xdev -type f -printf '%s %p\n' 2>/dev/null \
| sort -rn | head -20 | numfmt --field=1 --to=iec

-xdev and -x matter. Without them the walk crosses into /proc, /sys, network mounts and other filesystems, and you spend minutes measuring things that are not full.

Reclaim in order of least risk:

Destructivereclaim space
# 1. Journal - bounded, reversible only from backup
sudo journalctl --disk-usage
sudo journalctl --vacuum-size=500M
sudo journalctl --vacuum-time=7d

# 2. Package caches - always safe to rebuild
sudo apt-get clean          # Debian/Ubuntu
sudo dnf clean all          # RHEL family

# 3. Rotated logs older than the retention policy
sudo find /var/log -type f -name '*.gz' -mtime +14 -print   # LOOK FIRST
sudo find /var/log -type f -name '*.gz' -mtime +14 -delete

# 4. Old kernels (Debian) - keep at least the running one and one spare
uname -r
sudo apt-get --purge autoremove

Step 4: Inode exhaustion - find the count

Inodes run out when something creates millions of small files: a session directory, a mail spool, a cache with no eviction, a failing job writing one file per retry.

Read-only / Safeinode census
df -i

# Which top-level directory holds the file count
sudo du -x --inodes --max-depth=2 / 2>/dev/null | sort -rn | head -20

# Portable equivalent if du lacks --inodes
for d in /var/*; do printf '%8s %s\n' \
"$(sudo find "$d" -xdev -printf . 2>/dev/null | wc -c)" "$d"; done | sort -rn | head

Common inode sinks, in the order they usually turn up: /var/spool/postfix, PHP or application session directories, /var/lib/php/sessions, unrotated per-request log files, .git objects in a runaway CI checkout, and Docker/Podman overlay layers.

Destructivereclaim inodes
# Look first
sudo find /var/lib/php/sessions -type f -mtime +2 | wc -l

# Then delete, in batches so the shell does not choke on argument length
sudo find /var/lib/php/sessions -type f -mtime +2 -delete

# Mail spool - inspect before touching; queued mail is data
sudo postqueue -p | tail -1

Step 5: The emergency reserve

Every ext filesystem reserves 5% of blocks for root by default. That is why a filesystem at “100%” often still lets root write while unprivileged services fail — and it is the headroom that lets you run these commands at all.

Read-only / Safetune2fs -l
sudo tune2fs -l /dev/mapper/vg0-var | grep -E 'Reserved block count|Block count|Free blocks'

Lowering the reserve buys minutes and costs you the safety margin you are currently standing on. Treat it as a last-resort action taken knowingly, not a routine reclaim:

Configuration changetune2fs -m
# Only on a data filesystem, never on / - and reverse it afterwards
sudo tune2fs -m 1 /dev/mapper/vg0-var

Step 6: Verify, then fix the cause

Read-only / Safeverify
df -h /var
df -i /var
sudo lsof +L1 /var | wc -l

# A real write test as the service account
sudo -u www-data sh -c 'echo ok > /var/www/.writetest && rm /var/www/.writetest' \
&& echo 'write OK'

Reclaiming space is not the fix. The fix is one of:

  • Rotation. A logrotate rule with size or daily plus rotate N, and copytruncate or a postrotate reopen so the daemon does not hold the deleted file.
  • Retention. SystemMaxUse= in /etc/systemd/journald.conf, application-level log retention, cache eviction.
  • Quota. A per-user or per-project quota so one writer cannot consume the whole filesystem.
  • Monitoring. Alert on the trend, not the threshold. A filesystem that fills at 4 GB/day should page you three days out, not at 95%.
Configuration changejournald retention
# /etc/systemd/journald.conf
# SystemMaxUse=1G
# SystemKeepFree=2G
sudo systemctl restart systemd-journald
journalctl --disk-usage

Common patterns

SymptomLikely causeResolution
df 100%, du much smallerDeleted-but-open fileslsof +L1, then reload or truncate the fd
df has space, writes failInodes exhausteddf -i, find the file-count sink
Space returns then vanishes in minutesRunaway writerFind and stop the writer; do not keep reclaiming
Root can write, service cannot5% reserved blocksYou are at real 100%; reclaim now
/var/log huge after an incidentDebug logging left onTurn it off; that is the cause
Only /boot fullOld kernels accumulatedPurge old kernels, keep running + one
Deleting files frees nothingSnapshot or open fd holds the blocksCheck snapshots and lsof +L1

Knowledge check

Knowledge check · 4 questions

  1. Q1. `df -h /var` reports 100% used. `du -sxh /var` reports 12 GB on a 40 GB filesystem. You delete another 2 GB of logs and df still reports 100%. What is happening?

  2. Q2. A service reports "No space left on device" but `df -h` shows 60% free on that mount. What do you check next?

  3. Q3. You can add inodes to an existing ext4 filesystem without recreating it.

  4. Q4. Which actions are appropriate first responses to a full /var on a production host? Select all that apply.

Passing score: 75%. Answers are checked in this browser.

References

  1. lsof(8)
  2. journalctl(1) - vacuum options