Jitsumi
Back to the blog

Linux Disk Still Full After Deleting Files? Check Open Handles

Find deleted files that still occupy Linux disk space, identify their open handles, and release them safely. Includes a tested 16 MiB demonstration.

A crossed-out directory entry no longer names a file, but an active process still has an open handle pointing to its allocated blocks.

You delete a large log file, but Linux still reports almost no free space. Deleting more files at random is the wrong next step.

First check the affected filesystem, then look for deleted files that a process still holds open. On Linux, removing the last filename does not necessarily release the underlying file immediately. An open file descriptor can keep it alive until that reference is closed. The Linux unlink manual describes this distinction explicitly.

This guide is for people administering their own GNU/Linux machine or server. It uses GNU df and du, plus lsof for diagnosis. The optional Python example creates only its own small test file. It is not a reason to experiment on a full production filesystem, and none of the diagnostic commands below delete application data.

Find the filesystem that is actually full

Start with an existing directory where writes are failing. For a problem under /var/log, for example:

df -hT /var/log
df -i /var/log

In the first result, read Filesystem, Type, Avail and Mounted on. The path is a way to select a filesystem; it does not mean the displayed usage belongs only to that directory. -h changes the units and -T shows the filesystem type. The GNU df manual documents these options.

The second command checks inode availability rather than data-block usage. On a filesystem with a finite inode supply, running out of inodes is a different problem from running out of bytes. Do not assume deleting one large file will resolve a shortage caused by very many small files.

Use the mount point from that output for the next comparison. If it is /, run:

sudo du -xhd1 /

For a separate /var filesystem, use /var instead. This asks GNU du to total reachable files while skipping directories on other filesystems. -d1 limits the displayed depth, not the work required to calculate the totals; a large tree can still take time and generate substantial disk activity. The GNU du manual explains the depth and filesystem options.

Do not hide permission or read errors just to get a cleaner total. A partial scan is not a reliable basis for declaring space “missing.” Also avoid comparing df / with du of only your home folder: those commands are looking at different scopes.

Why df can stay high while du falls

df reports filesystem accounting. du walks the names it can reach. Once a file's last directory entry is removed, that walk no longer finds it, but a running process may still refer to the file. The lsof project's explanation of unlinked open files identifies exactly this source of otherwise hard-to-find usage.

Consider an application writing a log. Removing the log's name does not instruct the application to close its already-open descriptor or begin writing somewhere else. You have changed the directory, not the application's logging configuration.

This is a possible explanation, not a complete equation for every df/du discrepancy. Scope, permissions and the storage stack still matter. Establish an actual retained file before deciding which process needs attention.

Find the process without stopping it

If lsof is not installed, obtain your distribution's package through its normal package-management process. Do not attempt an installation on a critically full filesystem without first arranging enough working space.

To list unlinked open files visible to an authorised administrator:

sudo lsof -nP +L1

+L1 selects open files whose link count is below one: zero. -nP avoids network-name and port-name conversion. For the root filesystem specifically, the documented filesystem-filtered form is:

sudo lsof -nP +aL1 /

Replace / with the actual mount point, not an arbitrary subdirectory. The lsof manual documents both this form and the selection rules.

Read the output as evidence, not a kill list:

Field

What it helps establish

COMMAND / PID

Which process to investigate; not an instruction to stop it.

FD / TYPE

The descriptor and object type. REG means a regular file.

DEVICE / NODE

The filesystem device and inode identifying the file.

SIZE/OFF

A size or offset, not a guaranteed amount of reclaimable disk space.

NLINK / NAME

Zero remaining hard links and the reported name, often ending in (deleted).

A large REG entry with NLINK 0 on the affected filesystem deserves investigation. The same device-and-inode pair appearing under several descriptors is still one file; summing every row can count it repeatedly. Conversely, a familiar filename is not enough to identify the current file, especially after a log has been recreated.

Keep warnings visible. Lack of a result is not proof that no process holds a file: permissions, process visibility and a changing system can limit the inspection. Run the diagnosis in the appropriate host or administrative environment rather than treating a container's restricted view as a full host inventory.

Release the reference through its owner

Before changing the process, establish what it does, whether the deleted data is still needed, and whether interrupting it is acceptable. If the file was deleted accidentally and matters, stop here and seek recovery help before closing its last reference. Releasing space and preserving the remaining accessible data can be competing goals.

For a desktop application, save your work and close it normally. For a managed service, use its documented log-reopen, rotation or maintenance procedure. A generic reload is not a promise that the application will reopen its logs. In particular, systemctl daemon-reload reloads systemd's unit definitions; it does not tell an application to reopen a deleted file. The systemctl manual distinguishes manager reloads from service reloads and restarts.

Where a restart is necessary, identify the correct service, coordinate the interruption and check its recovery procedure first. There is deliberately no copy-and-paste restart command here: a process name from lsof is not automatically the service unit you should stop.

Do not default to kill -9, broad process termination or truncating a path under /proc/PID/fd. The Linux proc descriptor documentation explains that these entries refer to files the process actually has open. Writing through one is not harmless removal of a stale shortcut; it can alter the underlying file while the application is using it.

Then repeat the same lsof check and df -hT command. Confirm both that the retained file is no longer present in the relevant listing and that free space has changed on the same filesystem. Check the application's health and its new log output too.

A restart is not an unconditional guarantee. Another process may retain a reference, and services can deliberately store descriptors with systemd across restarts. The systemd file-descriptor store documentation describes that facility. If the file remains, investigate the surviving holder instead of repeatedly restarting unrelated services.

See the mechanism with a disposable 16 MiB file

This demonstration needs Linux, Python 3.8 or later and a writable directory with comfortable free space. Run it as your ordinary user in a disposable working directory—not on the filesystem currently in trouble. It writes 16 MiB of random data, removes only the new file's name, and pauses while its handle stays open.

Save this as open_file_demo.py:

"""Linux-only demonstration. Creates and removes its own 16 MiB test file."""
import os
import tempfile
from pathlib import Path


def main() -> None:
    if os.name != "posix" or not Path("/proc/self/fd").is_dir():
        raise SystemExit("Run this demonstration on Linux with /proc mounted.")
    with tempfile.TemporaryDirectory(prefix="open-file-demo-", dir=".") as folder:
        path = Path(folder) / "sample.bin"
        with path.open("xb+") as handle:
            for _ in range(16):
                handle.write(os.urandom(1024 * 1024))
            handle.flush()
            os.fsync(handle.fileno())
            before = os.fstat(handle.fileno())
            print(f"Before unlink: links={before.st_nlink}, bytes={before.st_size}")
            path.unlink()  # Only the fresh file created above.
            after = os.fstat(handle.fileno())
            print(f"After unlink: path exists={path.exists()}, links={after.st_nlink}")
            print(f"Same inode: {before.st_ino == after.st_ino}")
            print(f"Allocated bytes reported: {after.st_blocks * 512}")
            handle.seek(0)
            print(f"Still readable: {len(handle.read(1024)) == 1024}")
            print(f"Inspect: lsof -nP -a -p {os.getpid()} +L1")
            print(f"Descriptor: /proc/{os.getpid()}/fd/{handle.fileno()}")
            try:
                input("Press Enter to close the demo file: ")
            except EOFError:
                pass
    print("Closed demo handle; temporary directory cleaned up.")


if __name__ == "__main__":
    main()

Run it from that directory:

python3 open_file_demo.py

While it waits, a second terminal can run the exact Inspect command printed by the script. Here, -a matters: it combines the process-ID selection with the zero-link condition rather than asking for either set. The number belongs to this demonstration, not a PID to reuse later.

The printed descriptor path also identifies a read-only inspection target. You can use readlink on that path to inspect what it points to. Do not redirect output into it or truncate it. Return to the first terminal and press Enter to close the handle and finish cleanup.

What was actually tested

The script was executed in a Linux container using Python 3.13.5, kernel 6.18.57, and an overlay filesystem. Its first five output lines were:

Before unlink: links=1, bytes=16777216
After unlink: path exists=False, links=0
Same inode: True
Allocated bytes reported: 16777216
Still readable: True

The final cleanup message was also reached, and no demonstration directory remained. These results establish that this test file stayed allocated and readable after its name disappeared. They are not a benchmark or a measurement of production space recovered. lsof was not installed in the test environment; its commands above were checked against project documentation, not presented as locally tested output.

The allocated-byte calculation uses Linux's 512-byte st_blocks units, documented in the stat structure reference. Allocated size can differ from logical file length on other filesystems or for other files, so your result need not match this one. The script uses Python's documented file-descriptor operations and temporary-directory cleanup.

Do not enlarge the test until df visibly changes. A small allocation may disappear in rounded output, and concurrent activity can obscure a filesystem-wide difference. The inode, link count and readable handle demonstrate the point without filling a disk.

When open files are not the explanation

If the inspection finds no plausible retained file, return to the evidence rather than forcing this diagnosis. Check whether du completed without errors, whether you compared the entire correct filesystem, and whether inodes rather than blocks are exhausted. Snapshot-based storage, quotas and filesystem-specific accounting require their own checks; they are not fixed by deleting arbitrary logs.

For WSL 2, distinguish Linux free space from the Windows size of its virtual disk. Once Linux reports the space free, Windows-side compaction is a separate task. Jitsumi's guide to reclaiming Windows disk space from a WSL 2 virtual disk covers that next layer, including backup and shutdown precautions.

The useful finish line is concrete: identify the affected filesystem, find a real space holder, release it through a controlled application action, and verify that both storage and the application are healthy. A deleted filename alone does not establish that those steps have happened.

Sources

  1. Linux unlink: removing names and retaining open files.
  2. GNU df: filesystem and inode usage.
  3. GNU du: directory totals and filesystem boundaries.
  4. lsof manual: link-count and process selection.
  5. lsof FAQ: unlinked files, naming and visibility limitations.
  6. systemctl: reload and restart behaviour.
  7. systemd: descriptor storage across service restarts.
  8. Linux proc file descriptors and stat fields.
  9. Python OS interfaces and temporary files.

Share this post