du and df don’t match: deleted files still held open

A du df mismatch is the moment df says a file system is nearly full, but du, adding up every file it can find, comes to far less. The most common cause is a big file someone deleted while a running process still had it open: the name is gone, so du can’t see it, but the blocks stay in use until that process lets go, so df still counts them.

You’ll recreate this on a small test file system on rocky01, find the hidden file with lsof +L1 and /proc, and get the space back in the three ways you’ll use at work: restart or reload the service, truncate the open file when a restart isn’t allowed, or let logrotate do it properly. Then you’ll meet the other reasons du and df disagree, so you don’t blame a deleted file for every gap.

Where this lesson fits

Lesson 66 of 82 in Linux Administration · Phase 4: Troubleshooting step by step · Before this: Lesson 65, the Linux troubleshooting method · Next: Lesson 67, no space left on device with free space

TimeYou needBy the end you can
About 60 minutesrocky01 with lsof (installed in Lesson 23), the rsyslog rule for /var/log/saatapp.log from Lesson 29, and about 1 GB of space left on the root file systemExplain why du and df disagree, find the process holding a deleted file, get the space back safely, and spot the other causes of a gap

What it is, in one line

du adds up the files it can reach by name, df asks the file system how many blocks are in use, so a deleted file that a process still holds open is counted by df and invisible to du.

In Simple Words

The library book that left the catalogue

In a college library, the librarian removes a book’s card from the catalogue because it’s going to be thrown away. But a student still has that book open at a reading table. The shelf space it came from can’t be given to a new book until the student hands it back, even though nobody can find it in the catalogue any more.

Someone counting the catalogue cards says the library has plenty of room. Someone walking the shelves and the reading tables says it’s full. Both are right.

du counts catalogue cards (file names), df counts shelf space (blocks), and the student at the table is the process holding the file open.

Engineer's View

Names, inodes and open handles

A file on Linux is an inode (the size, owner, permissions and the list of data blocks) plus one or more names in directories that point to it. rm calls unlink(), which removes one name and lowers the inode’s link count. The kernel frees the inode and its blocks only when the link count is 0 and no process has the file open. Until then the file lives on, without a name, and the process can keep reading and writing it.

du walks the directory tree and adds up the files it finds by name. df asks the file system (the statfs() call) how many blocks are used and free. Most gaps between the two come from one of these:

CauseWhat you seeHow to check
Deleted file still opendf high, du lowsudo lsof +L1, ls -l /proc/PID/fd
Files hidden under a mount pointdf of the parent higher than du of itBind-mount the parent elsewhere and run du there
Reserved blocks (ext4, 5% by default)Size minus Used is more than Availsudo tune2fs -l DEVICE | grep -i reserved
File system metadata (journal, inode tables, XFS log)A small fixed gap on an empty file systemNormal; compare with a fresh file system of the same size
Sparse filesls -l shows a big size, du shows littledu --apparent-size against plain du
du crossing into other mountsdu higher than dfAlways use du -x to stay on one file system
Snapshotsbtrfs or ZFS snapshots hold old blocks; LVM snapshots use space in the VG, not in dfbtrfs subvolume list, zfs list -t snapshot, lvs Data%

lsof +L1 lists open files whose link count is less than 1, which is the “deleted but open” case, and it’s much faster on a busy server than lsof | grep deleted. lsof is in Ubuntu Server’s standard packages and a dnf install lsof away on a Rocky minimal install. On Ubuntu 26.04, du and df come from the Rust rewrite of coreutils (uutils); the options in this lesson are the same, but check the column layout before you parse their output in a script.

How it works, step by step

Here’s the life of a log file that gets deleted while its program is still writing to it. The diagram below follows the same five stages.

1A process opens the file

The app calls open() on app.log and gets back a file descriptor, a small number such as 3. From now on it writes to fd 3, not to the name. The inode has one name (link count 1) and one open handle.

2Someone removes the name

rm app.log removes the directory entry and the link count drops to 0. Because fd 3 is still open, the kernel keeps the inode and every block. The app has no idea anything happened and keeps writing, so the hidden file can even grow.

du df mismatch explained: what rm does to a file that a process still has open

3du and df stop agreeing

du walks the folder, finds no app.log and doesn’t count it. df asks the file system, which still has those blocks marked as used. That’s the whole mismatch: one tool counts names, the other counts blocks.

4Find who holds it

sudo lsof +L1 shows the command, PID, file descriptor and size of every open file with no name left, and the NAME column ends in “(deleted)”. The same facts sit in /proc: every process has a folder /proc/PID/fd with one symbolic link per open file, and a deleted one points to its old path with ” (deleted)” added. That’s handy on a server without lsof.

5Make the process let go

The space comes back when the last handle closes. Restarting the service closes every file it had. Many daemons reopen their log files on a signal instead, which is gentler: rsyslog on SIGHUP, Nginx with nginx -s reopen.

When neither is allowed, you can empty the deleted file through the handle with : > /proc/PID/fd/N, and the blocks are freed while the process keeps running. The picture below compares the four options.

du df mismatch fixes compared: restart, HUP, truncate the open handle, copy first

6Stop it happening again

The real fix is to never rm a log that a program still writes to. To empty a live log, truncate it in place with : > app.log or truncate -s 0 app.log, which keeps the same inode, so the program writes into a file that still has a name. Better still, let logrotate rotate it with a postrotate signal (as in Lesson 29), or with copytruncate for programs that can’t reopen their logs.

Truncating only works cleanly when the program opened the file in append mode (O_APPEND), which almost every logger does. A program that writes at its own remembered offset carries on at, say, byte 200 million, and the file becomes sparse: ls shows the old size again while du shows almost nothing. Break-It 1 shows this, and it’s also the known weakness of copytruncate.

Keep the other causes in mind before you go hunting. A gap of about 5% on a new ext4 file system is the reserved blocks. du bigger than df means du wandered into another mount, so add -x. A big gap with nothing in lsof +L1 often means files were written into a folder before a volume was mounted on top of it, which you’ll find in Break-It 2.

Hands-on lab: hide 200 MiB in a deleted file and get it back

Before you run any command, double-check it

Read every command twice before you press Enter. Check the target: the server name, the user, the disk or device name, the path and the sizes. These commands are for learning and were tested in our lab, not on your system. At work, run the read-only commands first, follow your change process, make sure a backup exists, and ask a second person to check anything that changes or deletes data. You run them at your own risk.

You’ll do all the filling on a 512 MiB test file system kept in a file, /var/tmp/lab66.img, mounted through a loop device, so nothing in vg_data or on the root file system gets near full. Work on rocky01 as student, and take a snapshot first (VBoxManage snapshot rocky01 take before-l66 on the laptop). Outputs that came from running these steps for real were only adjusted for user, PID and device numbers; yours will differ in those.

1Build a test file system you can fill

Run this on rocky01:

mkdir -p ~/lab66
truncate -s 512M /var/tmp/lab66.img
mkfs.ext4 -q -L lab66 /var/tmp/lab66.img
sudo mkdir -p /mnt/lab66
sudo mount -o loop /var/tmp/lab66.img /mnt/lab66
sudo chown student:student /mnt/lab66
df -h /mnt/lab66

You should see something like this:

Filesystem      Size  Used Avail Use% Mounted on
/dev/loop0      488M   24K  452M   1% /mnt/lab66

truncate -s 512M makes an empty sparse file, mkfs.ext4 formats it, and -o loop tells mount to attach it to the first free loop device, /dev/loop0 here. Keep test images in /var/tmp. On Ubuntu 26.04 /tmp is a tmpfs in RAM, so a 512 MiB image there would eat half your VM’s memory.

✅ Checkpoint: df shows /dev/loop0 mounted on /mnt/lab66, about 488M in size.

2Start an app that keeps its log open

Create the program (paste this whole block):

cat > ~/lab66/writer66.py <<'EOF'
#!/usr/bin/env python3
# writer66.py: a small "app" that keeps its log file open, like a real service
import os, sys, time
path = sys.argv[1]
mode = sys.argv[2] if len(sys.argv) > 2 else "a"
log = open(path, mode)
line = "2026-10-27 11:05:00 INFO batch 0042 settled, filler " + "x" * 971 + "\n"
for i in range(200 * 1024):
    log.write(line)
log.flush()
print(f"writer66: PID {os.getpid()} wrote 200 MiB to {path}", flush=True)
n = 0
while True:
    n += 1
    log.write(time.strftime("%Y-%m-%d %H:%M:%S") + f" INFO heartbeat {n}\n")
    log.flush()
    time.sleep(5)
EOF
python3 -m py_compile ~/lab66/writer66.py && echo syntax ok

Start it in the background:

mkdir -p /mnt/lab66/app
python3 ~/lab66/writer66.py /mnt/lab66/app/app.log &

You should see something like this:

[1] 2481
writer66: PID 2481 wrote 200 MiB to /mnt/lab66/app/app.log

The program opens app.log in append mode, writes 200 MiB of log lines, and then adds a heartbeat line every five seconds, holding the file open the whole time, like a Java app or a database with a debug log switched on. Write down your PID; the rest of the lab uses 2481.

✅ Checkpoint: writer66 printed its PID and “wrote 200 MiB”.

3Check that du and df agree, and read df properly

Run this:

ls -lh /mnt/lab66/app/app.log
du -sh /mnt/lab66
df -h /mnt/lab66
sudo tune2fs -l $(findmnt -no SOURCE /mnt/lab66) | grep -E 'Block count|Reserved block count|Block size'

You should see something like this:

-rw-r--r--. 1 student student 201M Oct 27 14:53 /mnt/lab66/app/app.log
201M	/mnt/lab66
Filesystem      Size  Used Avail Use% Mounted on
/dev/loop0      488M  201M  252M  45% /mnt/lab66
Block count:              131072
Reserved block count:     6553
Block size:               4096

du and df both say 201M, so right now there’s no mystery. Look at df’s own numbers though: 488M minus 201M is 287M, yet Avail is 252M. The difference is the reserved blocks, 6553 blocks of 4 KiB, about 25.6 MiB (5%), which only root can use.

That gap is normal on every ext4 file system and isn’t a du/df problem.

✅ Checkpoint: du and df both show about 201M, and you can explain why Avail is less than Size minus Used.

4Delete the log the wrong way

Run this:

rm /mnt/lab66/app/app.log
ls -l /mnt/lab66/app
du -sh /mnt/lab66
df -h /mnt/lab66

You should see this:

total 0
24K	/mnt/lab66
Filesystem      Size  Used Avail Use% Mounted on
/dev/loop0      488M  201M  252M  45% /mnt/lab66

The folder is empty and du counts 24K, but df still shows 201M used. This is the ticket you’ll get at work: “I deleted the big log and the disk is still full.” The writer is still running and still writing heartbeats into a file nobody can see.

✅ Checkpoint: du says 24K and df still says 201M used.

du df mismatch lab: lsof +L1 shows the deleted app.log held by python3 on rocky01
Example output from rocky01: du and df disagree, and lsof +L1 finds the deleted log

5Find the ghost with lsof +L1

Run this:

sudo lsof -a +L1 /mnt/lab66

You should see something like this:

COMMAND  PID    USER   FD   TYPE DEVICE  SIZE/OFF NLINK NODE NAME
python3 2481 student    3w   REG    7,0 209715348     0   13 /mnt/lab66/app/app.log (deleted)

+L1 selects open files with a link count below 1, -a means “and”, and the path limits it to this file system. NLINK 0 and “(deleted)” confirm it. FD 3w is file descriptor 3, open for writing, and SIZE/OFF is the hidden file’s size, 200 MiB plus a few heartbeat lines.

Run plain sudo lsof +L1 on a whole server; on rocky01 you may also see a few lines like /memfd:... (deleted), which are in-memory files used by systemd and others, not disk space.

✅ Checkpoint: lsof shows python3 with PID 2481 (yours), FD 3w and NLINK 0.

6Look inside /proc and save a copy

Run this (use your PID):

ls -l /proc/2481/fd
tail -n 1 /proc/2481/fd/3
find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null | grep lab66

You should see something like this:

lrwx------. 1 student student 64 Oct 27 14:53 0 -> /dev/pts/0
lrwx------. 1 student student 64 Oct 27 14:53 1 -> /dev/pts/0
lrwx------. 1 student student 64 Oct 27 14:53 2 -> /dev/pts/0
l-wx------. 1 student student 64 Oct 27 14:53 3 -> /mnt/lab66/app/app.log (deleted)
2026-10-27 14:53:44 INFO heartbeat 4
/proc/2481/fd/3 -> /mnt/lab66/app/app.log (deleted)

fd 0, 1 and 2 are the terminal, and fd 3 points to the deleted file. You can read it through that link as if it still had a name: tail printed the latest heartbeat. The find line is the “no lsof” way, the same command the Command Builder task gives you.

Because the data is still there, you can rescue it before you throw it away:

cp /proc/2481/fd/3 /var/tmp/app.log.saved
ls -lh /var/tmp/app.log.saved

You should see something like this:

-rw-r--r--. 1 student student 201M Oct 27 14:55 /var/tmp/app.log.saved

Copy to a different file system, never to the one that’s full. The same trick saves an important file that someone deleted by mistake, as long as a process still has it open.

✅ Checkpoint: /var/tmp/app.log.saved is 201M and ends with a heartbeat line.

7Free the space without stopping the app

Pretend this app can’t be restarted until the weekend. Empty the deleted file through the open handle:

Run this:

: > /proc/2481/fd/3
df -h /mnt/lab66
sudo lsof -a +L1 /mnt/lab66
sleep 6; sudo lsof -a +L1 /mnt/lab66

You should see something like this:

Filesystem      Size  Used Avail Use% Mounted on
/dev/loop0      488M   28K  452M   1% /mnt/lab66
COMMAND  PID    USER   FD   TYPE DEVICE SIZE/OFF NLINK NODE NAME
python3 2481 student    3w   REG    7,0        0     0   13 /mnt/lab66/app/app.log (deleted)
COMMAND  PID    USER   FD   TYPE DEVICE SIZE/OFF NLINK NODE NAME
python3 2481 student    3w   REG    7,0       37     0   13 /mnt/lab66/app/app.log (deleted)

: is the shell’s “do nothing” command, so : > /proc/2481/fd/3 opens the file for writing with truncation and closes it, which sets its size to 0. df dropped from 201M to 28K at once, and the app carried on: six seconds later the hidden file holds 37 bytes, one new heartbeat at the start. sudo truncate -s 0 /proc/2481/fd/3 does the same. You need sudo when the process belongs to another user.

Only truncate a file you are sure can be lost, such as a debug log, and only when the program writes in append mode. Truncating a database file, a queue file or anything the program reads back will corrupt it.

✅ Checkpoint: df shows 28K used while writer66 is still running.

8Get space back from a real service: rsyslog

Now the same mistake on a real daemon, with the local3 log from Lesson 29.

Run this on rocky01:

logger -p local3.info -t saatapp "order 1050 placed"
sudo ls -l /var/log/saatapp.log
sudo rm /var/log/saatapp.log
logger -p local3.info -t saatapp "order 1051 placed"
sudo lsof +L1 | grep saatapp

You should see something like this:

-rw-------. 1 root root 1843 Oct 27 15:01 /var/log/saatapp.log
rsyslogd 1108 root    9w   REG  253,0     1843     0 17305822 /var/log/saatapp.log (deleted)

rsyslogd still has the deleted file open on fd 9 (DEVICE 253,0 is rl-root), and “order 1051” was written into that hidden file, where nobody will ever read it. Tell rsyslog to reopen its files with SIGHUP, which is what the postrotate line in your Lesson 29 logrotate file does:

sudo systemctl kill -s HUP rsyslog
logger -p local3.info -t saatapp "order 1052 placed"
sudo ls -l /var/log/saatapp.log
sudo tail -n 1 /var/log/saatapp.log
sudo lsof +L1 | grep -c saatapp

You should see something like this:

-rw-r--r--. 1 root root 62 Oct 27 15:02 /var/log/saatapp.log
Oct 27 15:02:10 rocky01 saatapp[3120]: order 1052 placed
0

rsyslog closed the deleted file (freeing its blocks) and created a new saatapp.log with the next message. The new file gets rsyslog’s own default permissions rather than the 0600 that logrotate used, so check them with ls -l if the log is sensitive. sudo systemctl restart rsyslog works too and is what most runbooks say; the HUP is lighter because rsyslog keeps running and its forwarding connection to ubu01 stays up. The message logged between the rm and the HUP is gone for good, which is one more reason to never rm a live log.

✅ Checkpoint: lsof +L1 no longer shows saatapp.log, and the new /var/log/saatapp.log contains order 1052.

9Stop the app and confirm nothing is held

Run this:

kill %1
sudo lsof -a +L1 /mnt/lab66
df -h /mnt/lab66

You should see something like this:

[1]+  Terminated              python3 ~/lab66/writer66.py /mnt/lab66/app/app.log
Filesystem      Size  Used Avail Use% Mounted on
/dev/loop0      488M   28K  452M   1% /mnt/lab66

When a process ends, the kernel closes all its files, so stopping or restarting the holder is the cleanest fix of all. lsof printed nothing because no deleted file is open on /mnt/lab66 any more. Keep the test file system mounted for the Break-It section, and delete the saved copy with rm /var/tmp/app.log.saved.

✅ Checkpoint: lsof prints nothing for /mnt/lab66 and df shows it nearly empty.

Break It, Then Fix It

Both exercises use /mnt/lab66 from the lab, and the second one ends with the full cleanup. Neither touches lv_app, lv_logs or anything else later lessons need.

1Truncate a log the app doesn’t open in append mode

Start the writer again, this time in plain write mode, and empty its log the “safe” way:

python3 ~/lab66/writer66.py /mnt/lab66/app/app2.log w &
sleep 3
truncate -s 0 /mnt/lab66/app/app2.log
ls -lh /mnt/lab66/app/app2.log
sleep 6
ls -lh /mnt/lab66/app/app2.log
du -h /mnt/lab66/app/app2.log
df -h /mnt/lab66

You’ll get this:

-rw-r--r--. 1 student student 0 Oct 27 15:10 /mnt/lab66/app/app2.log
-rw-r--r--. 1 student student 201M Oct 27 15:10 /mnt/lab66/app/app2.log
4.0K	/mnt/lab66/app/app2.log
Filesystem      Size  Used Avail Use% Mounted on
/dev/loop0      488M   32K  452M   1% /mnt/lab66

The file “grew back” to 201M within six seconds. Without append mode the program keeps its own write position, about 200 MiB into the file, and its next heartbeat lands there. Everything before it is a hole that takes no blocks, so du says 4.0K and df is fine, but ls, backup tools and anyone who opens the file in less see 200 MiB of zero bytes.

This is how a “truncated” log can make a backup job slow or a log shipper send garbage.

Fix: see both sizes, then restart the writer so it starts at offset 0.

du -h --apparent-size /mnt/lab66/app/app2.log
ls -ls /mnt/lab66/app/app2.log
kill %1; rm /mnt/lab66/app/app2.log

You should see something like this:

201M	/mnt/lab66/app/app2.log
4 -rw-r--r--. 1 student student 209715274 Oct 27 15:10 /mnt/lab66/app/app2.log

--apparent-size reports what ls reports; plain du reports blocks on disk, and the 4 at the start of ls -ls is the same 4 KiB. For a program like this the right fix is a restart (or a config change to append mode), not a truncate. logrotate’s copytruncate has the same weakness.

✅ Checkpoint: You can explain why ls says 201M and du says 4.0K for the same file.

2Files hidden under a mount point

Write a file into a folder, then mount something on top of that folder:

mkdir /mnt/lab66/archive
dd if=/dev/zero of=/mnt/lab66/archive/old-batch.dat bs=1M count=120 status=none
sudo mount -t tmpfs -o size=64M lab66cover /mnt/lab66/archive
du -sh /mnt/lab66
df -h /mnt/lab66
findmnt -R /mnt/lab66

You’ll get this:

24K	/mnt/lab66
Filesystem      Size  Used Avail Use% Mounted on
/dev/loop0      488M  121M  332M  27% /mnt/lab66
TARGET               SOURCE     FSTYPE OPTIONS
/mnt/lab66           /dev/loop0 ext4   rw,relatime,seclabel
└─/mnt/lab66/archive lab66cover tmpfs  rw,relatime,seclabel,size=65536k

df says 121M used, du finds 24K, and lsof +L1 would show nothing, because no file is deleted. The 120 MiB file is still there, in the archive folder on the ext4 file system, but the tmpfs mounted on top covers it. On real servers this happens when a volume fails to mount at boot (or mounts late), an app writes into the empty mount point on the root file system, and the volume is mounted over it later.

Fix: bind-mount the parent somewhere else, where nothing is mounted on top, and look there.

sudo mkdir -p /mnt/lab66-raw
sudo mount --bind /mnt/lab66 /mnt/lab66-raw
du -sh /mnt/lab66-raw/archive
ls -lh /mnt/lab66-raw/archive
sudo umount /mnt/lab66-raw

You should see this:

121M	/mnt/lab66-raw/archive
total 120M
-rw-r--r--. 1 student student 120M Oct 27 15:14 old-batch.dat

A bind mount shows one file system again at a second place, without the mounts that sit on top of it. For the root file system on a real server, the same check is sudo mount --bind / /mnt/rootview and then sudo du -xsh /mnt/rootview/srv/*. Move or delete what you find, then unmount the view.

Clean up everything from this lesson:

sudo umount /mnt/lab66/archive
sudo umount /mnt/lab66
sudo rmdir /mnt/lab66-raw /mnt/lab66
rm /var/tmp/lab66.img
losetup -a

✅ Checkpoint: du of /mnt/lab66-raw/archive showed the hidden 121M, and after cleanup losetup -a prints nothing (mount -o loop frees the loop device on unmount).

This happened at work

The month-end batch and the 38 GB debug log

A colleague of mine, Ritu, was the on-call engineer at a pension fund administrator in Delhi when this happened. It was 11 p.m. on the last day of the month, with the pension credit batch running for about three lakh pensioners, and the batch server’s /var hit 100%. The bank file for the payouts had to be written to /var/batch/out before 2 a.m.

The junior on shift had already found the culprit, he thought: a 38 GB debug log from the payout engine, switched on during a test the week before. He deleted it with rm. df still said 100%. He then deleted three months of archived reports to make room, and df moved by 2 GB.

The batch failed at its next write with “No space left on device”.

Ritu ran du -xsh /var, which said 41 GB on a 80 GB file system, and then sudo lsof +L1. The payout engine’s JVM still had the deleted log open on fd 214, size 38 GB and growing. Restarting the engine mid-batch would have meant rerunning the whole credit file from the start, so she checked with the app team that the debug log was not needed, confirmed it was written in append mode, and ran sudo truncate -s 0 /proc/PID/fd/214. df dropped to 51% at once, and the batch was restarted from its last checkpoint at 12:40 a.m.

The next week the team switched debug logging off by default, moved the engine’s logs to their own logical volume, added a logrotate rule with a size limit, and put “never rm a log a process is writing; truncate it or rotate it” in the runbook. Their monitoring now runs lsof +L1 every 15 minutes and alerts when deleted-but-open files add up to more than 1 GB.

Common mistakes and troubleshooting

MistakeWhat happensWhat to do instead
Deleting a live log with rmThe name goes, the space stays, and new log lines vanish into the hidden fileTruncate it in place with : > file or let logrotate rotate it
Deleting more files when df doesn’t dropReal data is lost and the space still doesn’t come backRun sudo lsof +L1 as soon as du and df disagree
Using lsof | grep deleted on a busy serverIt takes minutes and lists every thread, memfd and libraryUse lsof +L1, or lsof -a +L1 /mountpoint
Truncating a file that isn’t a disposable logDatabase or queue files are corruptedRestart or stop the holder; truncate only logs you can lose
Truncating a log the program doesn’t append toThe file becomes sparse and ls shows the old size againRestart the program, or fix it to open its log in append mode
Running du without -xdu adds up other mounted file systems and disagrees the other wayUse du -xsh or du -xh --max-depth=1
Treating the 5% ext4 reserve as missing spaceTime wasted chasing a gap that is by designCheck tune2fs -l; lower it with tune2fs -m 1 only on data volumes

Interview questions

Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.

Fresher level

1. df says /var is 100% full, but du -sh /var shows only 40%. What is the most likely reason and how do you check?

Why they ask this: The classic first question on disk space. They want to hear “deleted file still open” and the right command, not “reboot it”.

How you might answer: Most likely a large file was deleted while a process still has it open, so df still counts its blocks but du can’t see a name. I check with sudo lsof +L1, which lists open files with no links left and shows the command, PID, FD and size.

Then I restart or reload that service so it closes the file, and df drops straight away. If lsof shows nothing, I look for files hidden under a mount point and check the reserved blocks.

2. What is the difference between deleting a log with rm and emptying it with truncate?

Why they ask this: They’re checking that you understand inodes and open files, and that you know the safe habit.

How you might answer: rm removes the name; if a program has the file open, the inode and its blocks stay until the program closes it, and the program keeps writing into a file nobody can see. truncate -s 0 or : > file keeps the same inode and name and sets the size to zero.

So for a log that is being written, I truncate or let logrotate handle it. I only rm a log once nothing has it open, which I can check with lsof.

2–5 years experience

3. You find a Java app holding a 30 GB deleted log, and the business won’t allow a restart until midnight. What are your options?

Why they ask this: They want options with their risks, not one magic command.

How you might answer: First I confirm it’s a log, not data, and that the app writes it in append mode, by asking the app team and looking at the logging config. If the content might matter, I copy it out through /proc/PID/fd/N to another file system.

Then I free the space with sudo truncate -s 0 /proc/PID/fd/N. If it isn’t append mode, the file turns sparse but the space is still freed. I note it in the ticket and restart at midnight so the handle is closed properly, and I fix the log rotation so it doesn’t come back.

4. After someone truncated a log, ls -l shows it at 20 GB again a minute later, but df looks fine. What happened?

Why they ask this: This checks understanding of file offsets, sparse files and apparent size, which trips up many experienced admins.

How you might answer: The program didn’t open the file in append mode, so after the truncate it carried on writing at its old offset, about 20 GB in. The file is now sparse: du -h shows only the blocks in use, while du --apparent-size and ls show 20 GB.

Disk space is fine, but backups and log shippers may read gigabytes of zeros. The proper fix is restarting the program so it starts at offset 0, and switching it to append mode or rotating with a signal instead of copytruncate.

Senior level

5. du and df disagree on a server and lsof +L1 shows nothing. Walk me through the other causes you’d check.

Why they ask this: Seniors should know the less common causes and how to prove each one quickly.

How you might answer: I’d check whether files are hidden under a mount point: sudo mount --bind / /mnt/rootview and du -xsh on the folders that are mount points. Then the reserved blocks with tune2fs -l on ext4, and whether du was run with -x. On btrfs or ZFS I’d look at snapshots, which hold blocks that no current file owns.

I’d also check for processes in other mount namespaces, such as containers, which plain lsof on the host may still show but with paths that look different. Finally, a file system error can leave blocks marked used, which an fsck or xfs_repair in a maintenance slot would find.

6. How would you prevent deleted-but-open files from causing outages across a fleet?

Why they ask this: They want monitoring and standards, not only the fix.

How you might answer: I’d add a monitoring check that sums the size of lsof +L1 entries per file system and alerts above a threshold, alongside the usual df alerts. Every service gets a logrotate rule that either signals the service to reopen its logs or uses copytruncate where the app writes in append mode.

I’d also put application logs on their own volume so a runaway log can’t fill / or /var, and make “truncate, don’t rm, a live log” part of the runbook and onboarding, with this exact lab as the training.

Quick quiz

Q1. Why can df show a file system as full when du shows it nearly empty?

  1. du is slower than df
  2. A deleted file is still held open by a process, so its blocks are still in use
  3. df counts files twice
  4. du ignores files owned by root
Show answer

B — du counts files by name; df counts used blocks. A deleted but open file has blocks and no name.

Q2. Which command lists open files that have been deleted?

  1. lsof -i
  2. lsof +L1
  3. df -i
  4. du -x
Show answer

B — +L1 selects open files whose link count is below 1.

Q3. A service can’t be restarted, but its deleted 20 GB debug log must go. What frees the space while it keeps running?

  1. rm -f /proc/PID/fd/N
  2. sudo truncate -s 0 /proc/PID/fd/N
  3. sync
  4. kill -STOP PID
Show answer

B — Truncating through the open handle empties the hidden file. You cannot rm a /proc fd link.

Q4. What does sudo systemctl kill -s HUP rsyslog do here?

  1. Stops rsyslog for good
  2. Makes rsyslog close and reopen its log files, releasing a deleted one
  3. Deletes old logs
  4. Reloads systemd units
Show answer

B — rsyslog reopens its output files on SIGHUP, which is what logrotate’s postrotate uses.

Q5. On a fresh ext4 file system, Size minus Used is bigger than Avail. Why?

  1. A deleted file is open
  2. About 5% of blocks are reserved for root
  3. The disk is failing
  4. du was run without -x
Show answer

B — ext4 reserves 5% by default; tune2fs -l shows the reserved block count.

Quick recap

  • A du df mismatch means du (names) and df (blocks) count different things.
  • rm removes a name; the blocks are freed only when no name and no open handle is left.
  • Find the holder with sudo lsof +L1 or ls -l /proc/PID/fd and look for “(deleted)”.
  • Get the space back by restarting or reloading the holder (SIGHUP for rsyslog), or, as a last resort, : > /proc/PID/fd/N for a log you can lose.
  • Truncate live logs instead of deleting them, and know that a non-append writer leaves a sparse file.
  • Other gaps: files under a mount point (check through a bind mount), ext4 reserved blocks, du without -x, snapshots.

Next lesson

Lesson 67: “No space left on device” with free space → You’ll fill a small file system with tiny files until it runs out of inodes while df -h still shows free space, and work through the other causes of that error: reserved blocks, quotas, a full tmpfs /tmp and more.

Further reading: du df mismatch

Want to go deeper into du df mismatch? Read the man7.org pages for du, df, unlink(2) and proc_pid_fd(5), and the lsof project page for the full option list. You can also see the full plan for this course on the Linux Administration course page.

Scroll to Top

This website uses cookies

We use a few cookies to keep this site working. If you allow it, we also use Google Analytics to count visits so we can improve our lessons. You can accept all, deny the optional ones, or choose what to allow.