The problem
When a small server runs out of disk, the first question is what grew. du and
ncdu show what is large now. They do not show what changed since yesterday or
last week. When a disk fills up again and again, the directory that grew has to
be searched for by hand each time.
How it works
Each run of diskdelta records the size
of every directory up to a chosen depth below a root directory (default 3
levels) and stores this snapshot as a JSON file in ~/.local/state/diskdelta.
It keeps the last 30 snapshots. It then compares the new snapshot with the
previous one and with the newest snapshot that is at least seven days old, and
prints both growth tables and the largest directories.
Sizes are measured like du reports them: allocated blocks, with hard links
counted once. Symlinks are not followed, and the scan stays on the filesystem
of the root directory.
A growth of 2 GB in /tmp/render-cache also makes /tmp and / grow by
2 GB. To avoid listing the same change three times, the growth table leaves out
a directory when one of its recorded subdirectories accounts for at least 90
percent of its change. The table then names the deepest directory that explains
the growth. When the change is spread over several subdirectories, the parent
stays in the table.
With --min-free 2G it prints ALERT and exits with status 2 when free space
is below 2 GB, so a script or cron job can react to the exit status. The README
suggests a daily cron entry:
30 4 * * * timeout 900 diskdelta / --depth 3 --min-free 2G >> ~/diskdelta.log 2>&1
This is the example from the README (the README marks the paths as illustrative):
$ diskdelta / --depth 3 --min-free 2G
2026-10-25 04:30 / free 3.1G
Growth since last run (2026-10-24 04:30, free then 5.6G, change -2.5G):
2.1G /tmp/render-cache
290.4M /home/dev/.cache/uv
61.0M /var/log/journal
Growth since 7 days (2026-10-18 04:30, free then 8.4G, change -5.3G):
2.1G /tmp/render-cache
1.9G /home/dev/.local/share/claude/versions
...
Largest directories:
14.2G /home
...
The tool needs Python 3.10 or newer and has no dependencies.
Testing
The scan tests write files of known size into a temporary directory and check which directories are recorded at a given depth and that parents are at least as large as their children. One test adds a hard link and a symlink that points back to the root; the hard link must be counted once and the loop must not be followed.
The growth tests use size tables. They check that the change is attributed to
the deepest directory, that a parent is hidden when a single child explains its
growth, that a parent stays when two children share the change, and that new
and shrinking directories are handled. Further tests cover the choice of the
seven-day baseline, size parsing (2G, 500M, 1.5GiB) and the human-readable
output.
The command-line test runs the tool three times on a temporary directory: the
first run reports "no snapshot yet", the second run lists a newly written log
directory in the growth table, and a run with --min-free 1000T prints
ALERT and returns 2. Another test checks that only the configured number of
snapshots is kept.
There are 14 tests. CI runs them on Python 3.10, 3.11, 3.12 and 3.13, together
with ruff check and ruff format --check, and runs the tool once on its own
checkout.
Limitations
- Directories are recorded only down to the chosen depth. Growth further down is attributed to the deepest recorded directory above it.
- Unreadable directories are skipped. For a full picture of
/the tool has to run as root. - Other filesystems mounted below the root directory are not included.
- The growth tables need earlier snapshots. The first run has none, and the seven-day comparison appears only once a snapshot of that age exists.
The code and installation instructions are at github.com/Jwrede/diskdelta.