I use du almost every week, usually in the exact same panicked context: a disk is filling up and I need to find out what’s actually eating the space, fast. It’s a deceptively simple command with a genuinely deep set of options once you get past du -sh. I want to walk through it properly — how it actually measures usage (which surprises people the first time), the full parameter set, and the workflows I lean on for real troubleshooting.
What du Does
du (disk usage) walks a directory tree and reports how much disk space each file and directory actually consumes on disk, recursively summing up to whatever level you ask for. This is a fundamentally different measurement from a file’s logical size — du reports the number of disk blocks allocated, not the byte count you’d see from ls -l.
Syntax
du [OPTION]... [FILE]...
du [OPTION]... --files0-from=F
Core Options
I tested all of these directly to confirm real output formatting:
| Option | Long form | Description |
|---|---|---|
-a | --all | Show sizes for files too, not just directories |
-h | --human-readable | Sizes in K/M/G/T instead of raw block counts |
-s | --summarize | Only show a total for each argument, not every subdirectory |
-c | --total | Print a grand total after processing all arguments |
--max-depth=N | Limit recursion depth in the summary output | |
-x | --one-file-system | Don’t cross into other mounted filesystems while recursing |
--exclude=PATTERN | Skip files/directories matching a glob pattern | |
-B SIZE | --block-size=SIZE | Report sizes in fixed units (e.g., -B M for megabytes) |
-L | --dereference | Follow symlinks rather than counting the link itself |
--apparent-size | Print logical file size (like ls) instead of actual disk block allocation | |
--time | Show the last modification time of each file/directory alongside size | |
-0 | --null | End each output line with NUL instead of newline, safe for scripting on odd filenames |
Tested Examples
I built a small directory with known file sizes to demonstrate exactly how output scales:
$ mkdir -p duf_test/sub1 duf_test/sub2
$ dd if=/dev/zero of=duf_test/file1.bin bs=1M count=5 # 5 MB
$ dd if=/dev/zero of=duf_test/sub1/file2.bin bs=1M count=3 # 3 MB
$ dd if=/dev/zero of=duf_test/sub2/file3.bin bs=1K count=200 # 200 KB
Default output (1K blocks)
$ du duf_test
204 duf_test/sub2
3076 duf_test/sub1
8404 duf_test
Human-readable
$ du -h duf_test
204K duf_test/sub2
3.1M duf_test/sub1
8.3M duf_test
Summarize only (single total)
$ du -sh duf_test
8.3M duf_test
This is the single most common invocation I run — du -sh <path> — to instantly answer “how big is this directory, total.”
Show every file, not just directories
$ du -ah duf_test
200K duf_test/sub2/file3.bin
204K duf_test/sub2
3.0M duf_test/sub1/file2.bin
3.1M duf_test/sub1
5.0M duf_test/file1.bin
8.3M duf_test
Limiting recursion depth
$ du -h --max-depth=1 duf_test
204K duf_test/sub2
3.1M duf_test/sub1
8.3M duf_test
This is invaluable on deep directory trees (like a node_modules folder or a large web root) where full recursive output would be thousands of lines — --max-depth=1 gives you exactly the top-level breakdown you actually want before drilling further.
Grand total across multiple arguments
$ du -ch duf_test/sub1 duf_test/sub2
3.1M duf_test/sub1
204K duf_test/sub2
3.3M total
Why du’s Numbers Don’t Match ls -l
This trips up almost everyone the first time. ls -l reports a file’s apparent size — the exact logical byte count of its content. du reports actual disk block allocation, which differs for two main reasons:
- Block rounding — filesystems allocate space in fixed block units (commonly 4 KB on ext4). A 1-byte file still consumes a full block on disk, so
dureports 4K even thoughls -lreports 1. - Sparse files — a file can have a huge logical size but contain large unwritten “holes” that consume no actual disk blocks. A 10 GB sparse file (common for VM disk images) might report
10Gfromls -lbut only a few hundred MB fromdu, becauseduonly counts blocks actually allocated on disk.
If you specifically want du to match ls -l logical sizes instead of block allocation, use --apparent-size:
du --apparent-size -h duf_test/file1.bin
How du Works Internally
du performs a recursive directory walk (functionally similar to what find does), calling stat() (technically lstat() by default, so it doesn’t follow symlinks unless -L is given) on every entry. The st_blocks field returned by stat() — the number of 512-byte blocks actually allocated to the file by the filesystem — is what du sums, then converts to whatever display unit you requested. This is why du can be genuinely slow on directories with millions of small files: it’s not reading file content, but it is issuing a system call per entry, and that per-call overhead dominates on huge trees, especially over network filesystems like NFS where each stat() is a round trip.
This also explains hard links: a file with multiple hard links pointing to the same inode is only counted once in du‘s totals by default (unless you use -l/--count-links to force counting each link separately), because du recognizes it’s the same underlying disk allocation.
Real-World Troubleshooting Workflow
When a disk fills up unexpectedly, this is the sequence I run, almost every time:
# 1. Confirm which filesystem is actually full
df -h
# 2. Find the largest top-level directories under root
du -h --max-depth=1 / 2>/dev/null | sort -rh | head -20
# 3. Drill into whichever directory looks disproportionate
du -h --max-depth=1 /var/log 2>/dev/null | sort -rh | head -10
# 4. Once you're close to the culprit, list individual files too
du -ah /var/log/myapp 2>/dev/null | sort -rh | head -20
I always pipe through sort -rh (reverse, human-numeric sort) — without it, du -h output is in directory-traversal order, not size order, and eyeballing which line is biggest across dozens of entries is slow and error-prone.
Finding the Top 10 Largest Files System-Wide
find / -xdev -type f -exec du -h {} + 2>/dev/null | sort -rh | head -10
I use find ... -exec du here rather than a single recursive du -ah /, because find lets me filter by type and stay on one filesystem (-xdev) cleanly, and this pattern scales better on very large trees since it avoids du building a full in-memory recursive summary before output.
Automation Example: Disk Usage Alerting Script
#!/bin/bash
set -euo pipefail
THRESHOLD_MB=1024
TARGET_DIR="/var/log"
while IFS=$'\t' read -r size path; do
size_mb=$(numfmt --from=iec "${size}" 2>/dev/null | awk '{print int($1/1048576)}')
if [ "$size_mb" -ge "$THRESHOLD_MB" ]; then
echo "ALERT: $path is using ${size} (over ${THRESHOLD_MB}MB threshold)"
fi
done < <(du -h --max-depth=2 "$TARGET_DIR" 2>/dev/null)
This kind of script is the backbone of many simple monitoring checks I’ve deployed before reaching for a full metrics stack — it’s cheap, dependency-free, and easy to run from cron.
Performance Optimization
- Avoid
du -aon huge trees unless you truly need per-file granularity; summarized (-s) or depth-limited output is dramatically faster since it still walks every file but avoids formatting and printing every single line. - Use
-x/--one-file-systemwhen scanning/to avoid recursing into mounted network shares or bind mounts, which can be extremely slow or even hang on stale NFS mounts. - Prefer
ncdu(not part of coredu, but built on the same underlying stat-walk concept) for interactive exploration — it caches the walk once and lets you browse results without re-scanning, which is much faster for repeated investigation of the same tree. - On very large filesystems, consider filesystem-native usage reporting instead:
btrfs filesystem du, or ZFS’szfs list -o space, which can read pre-computed accounting data rather than performing a live walk.
du vs Related Commands
| Command | Purpose |
|---|---|
du | Actual disk space consumed by files/directories, walked recursively |
df | Free/used space at the filesystem (mount point) level, from the kernel’s live block accounting, not a directory walk |
ls -l | Logical/apparent file size, not actual disk allocation |
stat | Detailed metadata for a single file, including both apparent size and st_blocks |
ncdu | Interactive, cached, browsable version of du‘s output |
A distinction worth internalizing: du and df can legitimately disagree. df reflects the filesystem’s live block accounting (including space held by deleted-but-still-open files, filesystem reserved blocks, and journal overhead), while du only reflects what it can see by walking the visible directory tree. A classic cause of “df says full but du can’t find the files” is a large file that’s been deleted but is still held open by a running process — the space isn’t freed until that file descriptor closes, and du can’t see a deleted file at all, but df still counts the space as used. lsof +L1 or lsof | grep deleted is the standard way to hunt that down.
Security Implications
du requires read and execute permission on directories to traverse them; running it as an unprivileged user against directories you don’t own will silently skip content you can’t read and may under-report actual usage without necessarily erroring loudly (depending on the directory’s permission structure), which can be misleading during a “what’s using my disk” investigation — running as root gives a complete picture when needed. Also worth knowing: because du performs a full filesystem tree walk, running it against attacker-controlled paths (deeply nested or maliciously crafted directory structures) can be used as a low-effort denial-of-service vector against automated scripts that shell out to du on user-supplied paths without limits — apply --max-depth, timeouts, or path validation in any automation that runs du against untrusted input.
Distribution Compatibility
du is part of GNU coreutils and ships identically across Debian, Ubuntu, Fedora, RHEL/CentOS, Arch, and openSUSE. BusyBox-based systems (Alpine, embedded distros) include a lighter du implementation with a smaller flag set — notably, --max-depth and --apparent-size are supported in BusyBox’s version, but some GNU-specific long options may be missing, so scripts intended to be portable to Alpine containers should stick to the short-flag core options (-s, -h, -a, -c).
du and Compression-Aware Filesystems
On filesystems that support transparent compression — Btrfs with compress=zstd, or ZFS with compression=lz4 — du‘s default behavior (reporting st_blocks, actual allocated blocks) reflects the compressed on-disk size, not the logical uncompressed size of the file’s content. This can produce genuinely surprising results: a 100 MB log file full of repetitive text might report as 8 MB under du on a compressed Btrfs subvolume, while du --apparent-size on the same file would report the full 100 MB logical size. Neither number is “wrong” — they’re answering different questions (actual disk consumption versus logical content size), but conflating the two is a common source of confused capacity planning on compressed filesystems. When auditing disk usage on Btrfs or ZFS specifically, I lean on the filesystem’s own native usage-reporting tools (btrfs filesystem du, zfs list -o space) alongside plain du, since they’re aware of shared blocks between snapshots in a way du‘s simple per-file walk fundamentally can’t be.
Excluding Paths During a Scan
For large trees where certain subdirectories are irrelevant to the investigation (build artifacts, .git history, cache directories), --exclude keeps output focused and meaningfully speeds up the scan:
du -sh --exclude='.git' --exclude='node_modules' /home/dev/projects/*
I use this constantly on development machines specifically to answer “how much space are my actual project files using” without the noise of regenerable build output and dependency trees, which frequently dwarf the source code itself in raw byte count.
Combining du with find for Age-Aware Cleanup
A pattern I rely on for cleaning up old build artifacts or log rotation leftovers combines find‘s time filtering with du‘s size reporting:
find /var/log -name "*.log.gz" -mtime +30 -exec du -h {} \; | sort -rh | head -20
This surfaces the largest old compressed logs specifically, rather than just the largest logs overall, which is usually the more actionable list when the goal is reclaiming space from things safe to delete.
Summary
du is one of those commands whose basic form (du -sh) you’ll use constantly, but whose deeper flags — --max-depth, --apparent-size, -x, -c — are what actually make it a serious troubleshooting tool rather than a one-liner. Understanding that it measures real block allocation rather than logical size, and that it can diverge from df, will save you real confusion the first time a “full” disk doesn’t match what du reports.
References
- GNU Coreutils Manual —
duinvocation: https://www.gnu.org/software/coreutils/manual/html_node/du-invocation.html - Linux man-pages —
du(1): https://man7.org/linux/man-pages/man1/du.1.html - Linux man-pages —
stat(2): https://man7.org/linux/man-pages/man2/stat.2.html - Red Hat Documentation — Monitoring Disk Usage: https://access.redhat.com/documentation/