Linux command: du
A disk is full. You run df -h and it says 95 percent. You run du -sh / and the numbers do not add up, sometimes by hundreds of gigabytes. Neither command is broken and neither is lying. They are answering two different questions, and the gap between their answers is one of the most useful things a Linux administrator can understand. This article is about what du actually counts, why that is not what you assumed, and how to find the thing that filled your disk.
1. The Basics
du walks a directory tree and adds up how much space the files in it occupy. That sentence contains three assumptions worth taking apart, because each one is where people go wrong.
1.1 The Simplest Possible Use
Two options do ninety percent of the work, and you will almost always want both:
| Option | Short for | What it does |
|---|---|---|
-s |
summarize | One total per argument, instead of one line per directory |
-h |
human-readable | K, M, G instead of a raw block count |
$ du -sh /var/log
1.4G /var/log
Without -s you get a line for every directory underneath, which on a big tree is thousands of lines scrolling past. Without -h you get a number in 1024-byte blocks, which nobody wants to divide in their head.
The single most useful invocation in this whole article is this one, and it is worth committing to memory:
$ du -h -d 1 /var | sort -h -r
8.2G /var
6.1G /var/lib
1.4G /var/log
403M /var/cache
...
-d 1 limits the output to one level down, and sort -h -r puts the biggest first. Run that, step into the biggest directory, run it again. Three or four rounds and you have found whatever ate the disk.
1.2 du Counts Blocks, Not Bytes
Here is the first assumption to abandon. du does not report how many bytes are in your files. It reports how much space the filesystem allocated to hold them, and those are different numbers.
$ echo -n "x" > tiny.txt # a one-byte file
$ du --apparent-size -b tiny.txt
1 tiny.txt # one byte of content
$ du tiny.txt
4 tiny.txt # four KILOBYTES on disk
$ stat -c '%s bytes, %b blocks of %B' tiny.txt
1 bytes, 8 blocks of 512
A filesystem hands out space in blocks, typically 4096 bytes on ext4. A one-byte file gets a whole block, and 4095 bytes of it are wasted. That is not a bug, it is how block allocation works, and it means a directory of many tiny files costs far more than the sum of their contents. Section 7.4 puts numbers on that.
Newer filesystems complicate this in both directions. Extents, inline data for very small files, and tail packing can all pack things more tightly than the flat block model suggests, while compression can make the allocated size smaller than the content. The 4 KB rule is a good default mental model for ext4 and a starting point elsewhere, not a law.
One more thing is an object with a size of its own: the directory. It has to store the names it contains, so it costs space even when it is empty, and it grows as you fill it:
$ mkdir emptydir
$ du -h emptydir
4.0K emptydir # empty, and already 4 KB
$ du -h --apparent-size emptydir
0 emptydir # no content at all
$ for i in $(seq 1 3000); do : > emptydir/file-with-a-longish-name-$i; done
$ stat -c '%s bytes' emptydir
167936 # the directory itself is now 164 KB of names
So du is not adding up file contents. It is adding up allocated space across every object it walks, directories included, which is another small reason its total will not match a sum of file sizes.
1.3 du and df Answer Different Questions
This is the distinction the whole article turns on. Say it out loud once and a lot of confusion disappears:
du | df | |
|---|---|---|
| Question | "How much space do these files use?" | "How much space does this filesystem have left?" |
| Method | Walks the tree and adds up every file it can see | Asks the filesystem for a number it already keeps |
| Scope | A directory | A whole filesystem |
| Speed | Slow: it must look at everything | Instant, whatever the size of the disk |
| Blind to | Deleted-but-open files, other people's unreadable directories, anything hidden under a mount | Nothing, but it cannot tell you which files |
The speed difference is not subtle, and it follows directly from the method:
$ du -sxh /usr/share 0.68 s # 207,983 files, every one stat'ed
$ df -h / 0.00 s # one question to the filesystem
Back to topThe right mental model:
dfasks the filesystem how much room is left.duasks the files how much room they take. When those two disagree, the difference is made of thingsducannot see, and section 7.1 is a catalogue of them.
2. Where the Name Comes From
du is disk usage and df is disk free. Two letters each, in the style of a time when every character had to be typed on a teletype and storage for the command name itself was worth worrying about.
The names have quietly drifted. Modern coreutils no longer says "disk" in either place, and the two descriptions on your own machine do not even agree with each other:
$ du --help | head -3 | tail -1
Summarize device usage of the set of FILEs, recursively for directories.
$ man du | sed -n '/^NAME/,+2p'
NAME
du - estimate file space usage
Both changes are honest. "Device usage" replaced "disk usage" because the thing underneath is often not a disk: it may be an SSD, a network share, a tmpfs living in RAM, or a virtual volume. And the man page says estimate, which is the most useful word in this article. du gives you a good number, not a true one, and sections 7.1 to 7.3 are three different reasons why.
The vocabulary you need is small, and every one of these terms causes a misunderstanding somewhere in this article:
apparent size how many bytes the file contains
device usage how much space the filesystem allocated for it
block the smallest unit a filesystem hands out, usually 4096 bytes
sparse file a file with holes: apparent size much larger than device usage
hard link one file with two names; du counts the space once
inode the filesystem's record of a file; you can run out of these
reserved space df counts as used that only root may touch
Keep apparent size and device usage apart in your head, because du reports the second by default while ls -l reports the first. Section 6.2 shows a file where they differ by a factor of infinity.
3. A Short History
Both commands are as old as Unix itself. Not "early Unix": the actual first release.
| Era | Milestone |
|---|---|
| 3 November 1971 | du and df both appear in Version 1 AT&T Unix, credited to Ken Thompson and Dennis Ritchie. |
| Early 1970s | They predate the C language rewrite of Unix. These commands are older than the language most of the system is now written in. |
| 1980s | POSIX standardises both. -s, -a and -k come from that era and work everywhere. |
| 1990s | GNU reimplements them for coreutils, adding -h, -d, --exclude, --apparent-size and the rest of what this article uses. |
| 2000s onwards | Disks grow faster than du gets quicker, and interactive tools such as ncdu appear to make the walk bearable. |
| coreutils 9.x | The help text changes "disk usage" to "device usage", acknowledging that the storage underneath is frequently not a disk. |
The GNU versions credit their authors, and the overlap between the two lists is not a coincidence:
$ man du | grep -A2 AUTHOR
Written by Torbjorn Granlund, David MacKenzie, Paul Eggert, and
Jim Meyering.
$ man df | grep -A2 AUTHOR
Written by Torbjorn Granlund, David MacKenzie, and Paul Eggert.
One historical detail still shows up in daily use. The default unit is 1024 bytes, but POSIX specifies 512, so the same command gives you numbers twice as large in a POSIX-strict environment:
$ du tiny.txt
4 tiny.txt
$ POSIXLY_CORRECT=1 du tiny.txt
8 tiny.txt # same file, 512-byte blocks
And POSIXLY_CORRECT is not the only variable involved. Three more change the unit, and du honours whichever it finds first:
$ du -s target
20484 target # the default, 1024-byte blocks
$ DU_BLOCK_SIZE=1M du -s target
21 target
$ BLOCK_SIZE=1M du -s target
21 target
$ BLOCKSIZE=1M du -s target
21 target
$ POSIXLY_CORRECT=1 du -s target
40968 target # 512-byte blocks: twice the number
So a bare number from du depends on four environment variables, any of which might be set in a cron environment, a CI runner or somebody's .bashrc. This is why -h, -k or -B is not optional in a script. The number has no unit attached to it, and the unit is decided by an environment you may not control.
The other portability question is which options exist at all. Most of what makes GNU du pleasant is a GNU extension, and inside a minimal container image you get BusyBox instead. Tested against BusyBox 1.38:
| Option | GNU coreutils | BusyBox 1.38 |
|---|---|---|
-s, -h, -a, -c, -x, -k, -m, -l, -L, -H |
Yes | Yes |
-d N |
Yes | Yes |
--max-depth=N |
Yes | No: unrecognized option |
--apparent-size |
Yes | No. Use -b, which both provide and which reports apparent size in bytes |
--exclude, -X |
Yes | No |
--inodes |
Yes | No |
-t, -S, -B, --si |
Yes | No |
The -b row is worth a moment, because the two spellings are not interchangeable in units. On a 50 MB sparse file, du --apparent-size prints 51200 (in 1 KB blocks) while du -b prints 52428800 (bytes). GNU defines -b as --apparent-size --block-size=1, and BusyBox behaves the same way, so -b is the portable choice as long as you expect bytes.
Note the depth rows too, because they matter for what you type every day. -d 1 works everywhere; --max-depth=1 does not. They do exactly the same thing, so there is no reason to learn the long one. That is why every example in this article uses -d.
$ docker run --rm busybox du --max-depth=1 /t
du: unrecognized option '--max-depth=1'
$ docker run --rm busybox du -d 1 /t
ok
macOS and the BSDs are a third dialect, with their own spelling for some of this. On anything that might not be GNU, stick to -s, -h, -k, -x, -a and -d, and check the local manual page for the rest.
4. Simple Use Cases
4.1 Sizing a Directory
$ du -sh /home/peter # one number
$ du -sh /home/* # one number per user
$ du -sh /var/www/*/ # one per site, the trailing slash skips files
That second form is the one to reach for when a shared server fills up. One line per account, and the culprit is usually obvious.
4.2 Finding the Big Directories
-d (short for max-depth) is what makes du usable on a large tree. Compare:
$ du -h sub # every directory, however deep
2.1M sub/a/b/c
4.1M sub/a/b
6.1M sub/a
8.1M sub
$ du -h -d 1 sub # only one level down
6.1M sub/a
8.1M sub
$ du -sh sub # the same as -d 0
8.1M sub
Then sort. sort -h understands the K, M and G suffixes that du -h produces, which is a relatively recent convenience and the reason this pairing works at all:
$ du -h -d 1 /var | sort -h -r | head -10
4.3 Including Files
By default du reports only directories. -a (short for all) adds the files:
$ du -ah sub | head -6
2.0M sub/a/f.bin
2.0M sub/a/b/c/f.bin
2.1M sub/a/b/c
2.0M sub/a/b/f.bin
4.1M sub/a/b
6.1M sub/a
Combined with sort this finds the single biggest files rather than the biggest directories:
$ du -ah /var/log | sort -h -r | head -10
4.4 Totals
-c (short for total) adds a grand total line:
$ du -shc /home/*/Downloads
2.3G /home/anna/Downloads
891M /home/peter/Downloads
3.2G total
Useful when the question is "how much would I get back by clearing all of these", which is a different question from "which one is biggest".
Back to top5. Moderate Use Cases
5.1 Excluding Things
--exclude takes a glob and skips anything matching it. This matters more than it sounds, because on a real server the interesting number is usually "how big is this, ignoring the parts I already know about":
$ du -sh /var/www/site --exclude='*/cache'
$ du -sh /home/peter --exclude='.cache' --exclude='.local/share/Trash'
$ du -sh /srv/project --exclude='node_modules' --exclude='.git'
For more than two or three patterns, put them in a file and use -X:
$ cat skip.txt
node_modules
.git
*.iso
$ du -sh -X skip.txt /srv/project
5.2 Thresholds
-t (short for threshold) hides everything below a size, which turns a wall of output into a short list:
$ du -h -d 3 -t 1G /var # only things over a gigabyte
$ du -ah -t 100M /home/peter # only files over 100 MB
A negative threshold inverts it, which is the less obvious half and occasionally exactly right:
$ du -ah -t -1k /srv/data # only entries SMALLER than 1K
That last one is how you find the directory full of a hundred thousand tiny files, which section 7.4 explains is a real problem and not just untidiness.
5.3 Depth Without Recursion: -S
-S (short for separate-dirs) reports each directory's own contents without adding in its subdirectories. Compare against the default:
$ du -h sub $ du -Sh sub
2.1M sub/a/b/c 2.1M sub/a/b/c
4.1M sub/a/b 2.1M sub/a/b
6.1M sub/a 2.1M sub/a
8.1M sub 2.1M sub
The left column accumulates; the right column does not. Use -S when you want to know which single directory holds the files, rather than which branch of the tree is heaviest. They are different questions and people often get the wrong one.
5.4 Counting Files Instead of Bytes
A filesystem can run out of two separate things: space, and inodes. When it runs out of inodes you get "No space left on device" while df -h cheerfully reports plenty free. --inodes finds the culprit using the same walk:
$ du -sh many
12K many # 500 empty files: almost no space
$ du -s --inodes many
501 many # but 501 inodes
$ du --inodes -d 1 /var | sort -n -r | head
Check whether that is your problem before you go hunting for big files:
$ df -i / # -i for inodes
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/mapper/... 120M 14M 106M 12% /
Mail queues, session directories and small-file caches are the usual suspects. A hundred thousand one-byte files use almost no space and a hundred thousand inodes.
5.5 The Recipe
Put together, this is the sequence for "the disk is full and I do not know why":
$ df -h # 1. which filesystem, and is it really full?
$ df -i # 2. or has it run out of inodes instead?
$ du -shx /* 2>/dev/null | sort -h -r | head # 3. top level
$ du -h -d 1 -x /var | sort -h -r | head # 4. descend into the winner
$ du -h -d 1 -x /var/lib | sort -h -r | head # 5. and again
$ du -ah -t 100M /var/lib/docker | sort -h -r # 6. finally, the files
Steps 1 and 2 take a second and rule out the two problems that look identical from a distance. Steps 3 to 6 are the same command with a different path each time. Section 6.1 explains the -x.
6. Advanced Use Cases
6.1 Staying on One Filesystem
-x (short for one-file-system) tells du not to cross into a different filesystem. Without it, a walk of / wanders into every mount you have: network shares, snap loopbacks, the FUSE mounts your desktop session created, and /proc.
This is not a theoretical tidiness argument. On this machine there are fourteen mounts under /run alone:
$ findmnt -rno TARGET,FSTYPE | grep '^/run/'
/run/lock tmpfs
/run/snapd/ns tmpfs
/run/user/1000 tmpfs
/run/user/1000/doc fuse.portal
/run/user/1000/gvfs fuse.gvfsd-fuse
...
$ du -sxh /run
5.4M /run # finished instantly
$ timeout 20 du -sh /run
(timed out at 20 seconds) # wandered into the FUSE mounts and stalled
A FUSE mount can be a remote server, a phone over MTP, or a cloud drive. du has no way to know that reading it means a network round trip per file, so it just sits there. Use -x on anything at or near the root of the tree, and note that it pairs naturally with df, which is also per-filesystem.
6.2 Apparent Size Versus Device Usage
Section 1.2 showed a small file using more space than it contains. Sparse files are the same idea running the other way, and the numbers are dramatic:
$ dd if=/dev/zero of=sparse.bin bs=1 count=0 seek=100M # a 100 MB hole
$ ls -lh sparse.bin
-rw-rw-r-- 1 peter peter 100M ... sparse.bin # ls sees 100 MB
$ du -h sparse.bin
0 sparse.bin # du sees nothing
$ du -h --apparent-size sparse.bin
100M sparse.bin # apparent size agrees with ls
The file claims to be 100 MB and occupies no blocks at all, because nothing has been written into it. The filesystem records "this range is zeroes" instead of storing a hundred megabytes of them.
This is not exotic. Virtual machine disk images, database files, container layer files and log files created by preallocation are routinely sparse. It has a practical consequence: copying a sparse file can make it enormous, because a naive copy writes out all the zeroes. cp --sparse=always and rsync -S preserve the holes; tar needs -S.
6.3 Hard Links Are Counted Once
If the same file appears twice in a tree under two names, du is clever enough to count the space once:
$ ln hl/orig.bin hl/link.bin # a hard link, not a copy
$ du -sh hl
11M hl # counted once
$ du -slh hl # -l for count-links
21M hl # counted twice
The default is right, and it is what makes du trustworthy on backup trees built with hard links, which is how rsync --link-dest and Time Machine style backups work. Ten daily snapshots of the same data really do occupy the space of roughly one, and du will tell you so.
The trap is at the edges. du only deduplicates within a single run. Size two directories in two separate commands and the shared file is counted in both, so the totals will not add up to what the filesystem shows.
6.4 Symbolic Links Are Not Followed
Hard links and symbolic links are different things and du treats them differently. A hard link is another name for the same data, so du counts it once. A symbolic link is a tiny file containing a path, so by default du counts the link and never looks at what it points to:
$ ln -s ../target tree/link # target holds 20 MB
$ du -sh tree
4.0K tree # the link itself, and nothing more
$ du -sh -L tree
21M tree # -L follows every symlink
$ du -sh -D tree
4.0K tree # -D follows only links NAMED on the command line
$ du -sh -D tree/link
21M tree/link # ... and now it is named
Four options control this, and the default is the safe one:
| Option | Behaviour |
|---|---|
-P |
Follow nothing. The default, and usually what you want. |
-L |
Follow every symlink found anywhere in the tree. |
-D (or -H) |
Follow only symlinks given as command-line arguments. |
-l |
Unrelated: count hard links repeatedly. Easy to confuse with -L. |
The default is right for the usual question, "how much space does this directory occupy?", because the target of a symlink is not inside the directory and deleting the directory would not free it. Reach for -L only when you mean "how much data is reachable from here", and be careful: a symlink can point anywhere, so -L can wander off into another filesystem, follow a link back into a parent and take a very long time, or double-count data you have already measured.
-D is the middle ground and the one to use when you deliberately pass a symlink as the argument: du -shD /var/www/current where current is a deployment symlink measures the release it points at, without following links found inside it.
6.5 Units
Three ways to control the number, and one of them surprises people:
$ du -h k.bin # powers of 1024
980K k.bin
$ du --si k.bin # powers of 1000
1.1M k.bin
$ du -B M k.bin # a unit you choose
1M k.bin
$ du -k k.bin ; du -m k.bin # 1K and 1M blocks, POSIX-portable
That is the same 1,000,000-byte file three times. -h divides by 1024 and gets 980K; --si divides by 1000 and gets 1.1M. Disk manufacturers use the second convention, which is most of the reason a "1 TB" disk shows up as 931 GB.
6.6 In Scripts
Two rules make du safe in a script. Always fix the unit, and always handle the filenames properly:
# fix the unit: never rely on the default
size_kb=$(du -sk "$dir" | cut -f1)
if [ "$size_kb" -gt 1048576 ]; then echo "$dir is over 1 GB"; fi
# handle any filename, including newlines and spaces
find /srv -type d -name node_modules -print0 | du -sh --files0-from=- -c | tail -1
The output format is a size, a single tab, and the path, so cut -f1 is the correct way to get the number, not awk '{print $1}', which breaks on a path containing spaces.
One caution that section 7.2 expands on: du writes its errors to standard error and its total to standard output, and the total does not include anything it failed to read. A script that discards stderr will get a confident number that is wrong.
7. Something Most Users Do Not Know
7.1 Why df and du Disagree
This is the question that brings people to this command, and the answer is not one thing. It is four, and they are cumulative.
Reserved blocks. Start with df disagreeing with itself. On this machine, its own three columns do not add up:
$ df --block-size=1 /
size = 2011859927040
used = 1808341385216
avail = 101246103552
used + avail = 1909587488768
missing = 102272438272 bytes, which is 5.1% of size
Around five percent of an ext4 filesystem is reserved for root by default. It is not free space as far as you are concerned, and it is not used by any file, so it belongs to neither column. That is why a disk can report "100% full" while several gigabytes are technically still there. You can reclaim it on a data volume, where nothing needs the safety margin:
$ sudo tune2fs -m 1 /dev/sdb1 # reduce the reserve from 5% to 1%
Do not do that on the root filesystem. The reserve exists so that a full disk does not stop root logging in and syslog writing, which is exactly when you need both.
Deleted files that are still open. This is the big one, and it is the reason for most dramatic disagreements. Unlinking a file removes its name; the space comes back only when the last process holding it open lets go:
$ dd if=/dev/zero of=ghost.bin bs=1M count=300
$ exec 9< ghost.bin # hold it open
$ rm ghost.bin # the name is gone
$ du -sh .
11M . # du sees nothing: there is no name to walk
df used before rm : 1724868 MB
df used after rm : 1724868 MB # df has not moved
df after closing : 1724568 MB # 300 MB back, at once
A log file deleted while the application still has it open is the classic version. The disk stays full until you restart the service, and no amount of du will show you the file. Find them like this:
$ sudo lsof -nP +L1 # +L1: link count below 1, meaning deleted
$ sudo lsof -nP | grep deleted
The fix is to restart the process holding the descriptor, or to truncate through /proc if you cannot: : > /proc/PID/fd/N.
Files hidden under a mount point. If a directory had files in it before something was mounted over it, those files still exist and still take space. They are simply unreachable, so du cannot count them:
$ sudo mkdir /mnt/tmp-check
$ sudo mount --bind / /mnt/tmp-check # see the real root, unobscured
$ sudo du -shx /mnt/tmp-check/var # count what is actually there
$ sudo umount /mnt/tmp-check
This one catches people who filled a directory before mounting a disk on it, which is easy to do during installation and invisible afterwards.
Filesystem overhead. Inode tables, journals and metadata occupy space that belongs to no file. On a large filesystem this is gigabytes, and it explains a permanent small gap rather than a sudden one.
And on a modern filesystem, the accounting itself. Everything above assumes ext4, where a block belongs to exactly one file. Btrfs, ZFS and XFS break that assumption in ways that make the question harder rather than the answer wrong:
| Feature | What it does to the numbers |
|---|---|
| Snapshots | Old versions of files still occupy blocks. No path leads to them, so du cannot see a byte of it. On a snapshotting server this is usually the largest part of the gap. |
| Reflinks and shared extents | Two files can share the same physical blocks. Both look full size to du, and deleting one frees nothing. |
| Compression | The file occupies fewer blocks than its content. du can report less than the apparent size on an ordinary, non-sparse file. |
| Copy-on-write | Overwriting part of a file allocates new blocks before releasing the old ones, so usage can rise while file sizes do not change. |
This machine runs ext4, where those do not apply: cp --reflink=always here fails with "Operation not supported". But on Btrfs or ZFS they are typically the dominant cause of a df/du gap, and no pathname-walking tool can account for them. Use the filesystem's own tools instead: btrfs filesystem usage, btrfs qgroup show, or zfs list -o space.
Put together: df counts everything the filesystem has committed. du counts what it can reach by walking names. The gap is reserved space, plus files with no name, plus files with no path, plus the filesystem's own bookkeeping, plus whatever your filesystem shares or compresses behind the scenes.
7.2 Throwing Away stderr Makes du Lie
Everybody writes 2>/dev/null to silence the permission warnings. Watch what that does to the answer:
$ du -sh /root 2>/dev/null
4.0K /root # looks like an empty directory
$ du -sh /root
du: cannot read directory '/root': Permission denied
4.0K /root # same number, now with a warning
That 4.0K is the directory entry itself. /root on this machine contains a Desktop folder and a snap directory, and neither is in the total. The number is not an error, it is an honest report of what du could see, and the part that told you it could not see everything went to stderr.
So when you run du -shx /* 2>/dev/null as a normal user, treat every number as a lower bound. Run it under sudo when the answer matters, and read the warnings when you do not.
7.3 du Does Not Double-Count Overlapping Arguments
Give du two paths where one contains the other, and it is smarter than you might expect:
$ du -sh sub/a
6.1M sub/a
$ du -sh sub/a/b
4.1M sub/a/b
$ du -sh sub/a sub/a/b -c
6.1M sub/a
6.1M total # not 10.2M
It tracks what it has already visited within a single run and refuses to add the same blocks twice. That is what makes du -shc /home/* reliable, and it is the same mechanism that handles hard links in section 6.3.
The limitation is the same too: this only holds within one invocation. Two separate du commands know nothing about each other.
7.4 Small Files Cost More Than They Weigh
Section 1.2 showed a one-byte file using 4 KB. Multiply that by a real directory and it stops being a curiosity:
100,000 files of 100 bytes each
apparent size: 100,000 x 100 = 10 MB
device usage: 100,000 x 4096 = 410 MB
Forty times larger. A mail spool, a PHP session directory, a thumbnail cache or a Git object store can occupy an order of magnitude more space than the data in it, and du --apparent-size against plain du tells you immediately whether that is what you are looking at:
$ du -sh /var/lib/php/sessions
$ du -sh --apparent-size /var/lib/php/sessions
# a big gap between these two means "many small files", not "much data"
And remember section 5.4: that same directory is also consuming an inode per file, which is a resource you can exhaust independently.
7.5 du Is Slow Because It Has To Be
There is no shortcut. To total a directory, du must stat every file in it, because no filesystem in common use keeps a running total per directory. That is why the cost scales with the number of files rather than their size:
$ find /usr/share -xdev | wc -l
207983
$ du -sxh /usr/share 0.68 s (cold)
$ du -sxh /usr/share 0.28 s (warm, from the kernel's cache)
$ df -h / 0.00 s
Two practical consequences. Running du on a large tree warms the cache, so a second run is much faster and any timing you take after the first is optimistic. And on a busy production server, a full du / is not free: it evicts useful data from the cache and generates a lot of metadata I/O. Prefer -x, prefer a narrow starting point, and consider ionice -c3 if you must do it during business hours.
7.6 Knowing Where du Stops
| When you need | Reach for |
|---|---|
To explore interactively rather than re-run du |
ncdu: one walk, then browse and delete |
| A readable overview of all filesystems | duf, or plain df -hT |
| A graphical view | baobab (Disk Usage Analyzer) |
| Files by age or name, not just size | find with -size, -mtime, -printf |
| Space used by deleted-but-open files | lsof -nP +L1, because du is blind to them |
| Per-user limits rather than per-user reports | Filesystem quotas: quota, repquota |
| Usage trends over time | Monitoring, not a command. du has no memory. |
ncdu deserves the top row and is worth installing everywhere. It does the same walk du does, once, and then gives you an interactive tree you can move around in and delete from, which turns the four-round descent from section 5.5 into arrow keys.
8. Best Practices
- Learn one command properly:
du -h -d 1 DIR | sort -h -r. Run it, step into the biggest entry, repeat. That finds almost anything in three or four rounds. - Add
-xwhenever you start near the root. Without itduwanders into network shares and FUSE mounts and can hang for good. - Check
df -handdf -ifirst. Full of data and out of inodes look identical from the application's side and need completely different fixes. - Do not trust a total that had its stderr thrown away. Every unreadable directory is silently missing from it. Use
sudowhen the answer matters. - Fix the unit in scripts.
du -skordu -B M, never a baredu, because the default is 1024 bytes normally and 512 underPOSIXLY_CORRECT. - Parse with
cut -f1, notawk '{print $1}'. The separator is a single tab, and paths contain spaces. - Compare
du -shwithdu -sh --apparent-sizewhen a number looks wrong. A big gap means many small files or a sparse file, and the two need opposite responses. - Exclude the parts you already know about.
--exclude='node_modules'and friends turn a useless total into an informative one. - Use
-tto cut the noise rather than piping throughheadand hoping. - When
dfanddudisagree by a lot, look for deleted-but-open files first.sudo lsof -nP +L1, then restart whatever is holding them. - Install
ncduon servers you look after. One walk, then an interactive tree, and you can delete from inside it. - Be considerate on production. A full
du /generates heavy metadata I/O and evicts the cache. Narrow the path, use-x, and considerionice -c3. - Prefer
-d 1to--max-depth=1. Identical behaviour, and the short form also works on BusyBox, which is what you get inside a minimal container. - Leave symlink following alone unless you mean it. The default counts the link and stops, which is right for "how big is this directory".
-Lis for "how much is reachable from here" and can wander. - On a snapshotting filesystem, reach for its own tools.
btrfs filesystem usageandzfs list -o spacesee whatducannot. - Read the manual once. It is short, and the word "estimate" in the very first line is the most honest thing in it.
$ man 1 du # short; note the NAME line
$ man 1 df # its counterpart, read them together
$ info coreutils 'du invocation'
$ man 1 ncdu
$ man 8 lsof # for the deleted-but-open case
Back to top9. Common Mistakes
9.1 Myth Versus Reality
| Myth | Reality |
|---|---|
"du and df should agree." |
They answer different questions. The gap is reserved blocks, deleted-but-open files, files under mount points, and metadata (section 7.1). |
"du tells me how many bytes my files contain." |
It tells you how many blocks they occupy. A one-byte file reports 4 KB. Use --apparent-size for the content. |
"ls -lh and du -h should match." |
ls shows apparent size, du shows device usage. On a sparse file that is 100M against 0. |
| "The disk is full, so I need to delete big files." | Maybe. Check df -i first: you may have run out of inodes, in which case you need to delete many files, not big ones. |
| "I deleted the log, so the space is back." | Not while a process holds it open. df will not move until you restart the service (section 7.1). |
"du -sh /* 2>/dev/null shows me everything." |
It shows what your user can read. The directories it was refused are silently missing from the totals (section 7.2). |
"du -sh a b double-counts if b is inside a." |
It does not. du tracks what it has visited within one run (section 7.3). |
| "Backups made with hard links use ten times the space." | du counts shared blocks once, so ten snapshots really do cost about one. -l turns the deduplication off. |
"du is slow because the directory is big." |
It is slow because of the number of files. It must stat every one; there is no stored total to read. |
"du -h and du --si are the same." |
1024 against 1000. The same file is 980K or 1.1M depending on which you pick. |
"A bare du number is in kilobytes." |
Usually, but it is 512-byte blocks under POSIXLY_CORRECT. Always pass -k, -h or -B. |
"du follows symlinks into the directories they point at." |
It does not. The default is -P: it counts the link, 4.0K, and stops. -L follows them, -D follows only the ones you name (section 6.4). |
"-l and -L are related." |
They are not. -l counts hard links repeatedly; -L dereferences symbolic links. Two different problems, one letter apart. |
"A bare du is in kilobytes unless POSIXLY_CORRECT is set." |
Four variables change it: DU_BLOCK_SIZE, BLOCK_SIZE, BLOCKSIZE and POSIXLY_CORRECT. Always pass a unit (section 3). |
"--max-depth=1 is the portable way to write it." |
It is the GNU-only way. BusyBox rejects it and accepts -d 1, which does the same thing everywhere. |
"du can always tell me what deleting this would free." |
Not with snapshots, reflinks or shared extents, where the blocks belong to more than one thing. Use the filesystem's own tools (section 7.1). |
| "Copying a file cannot make it bigger." | A sparse file copied naively writes out all its holes. Use cp --sparse=always or rsync -S. |
9.2 Other Traps to Avoid
- Running
du /without-xon a desktop. It will descend into/run/user/1000/gvfsand whatever that is mounting, and may never come back. - Reading
duoutput without sorting it. The order is directory-walk order, not size order.sort -h -ris not optional. - Forgetting
-hon one side of a pipe.sort -hneeds the suffixes thatdu -hproduces.du -k | sort -nalso works; mixing the two does not. - Using
awk '{print $1}'to extract the size. Works until a path contains a space. The field separator is a tab, socut -f1is correct. - Deleting from
ncduwithout looking. It deletes immediately and permanently, with no trash and no undo. - Using
-Lwithout thinking about where the links go. A symlink can point outside the tree, onto another filesystem, or back at a parent directory.-Lfollows all of them, so a scan you expected to take a second can wander a long way. - Blaming
duon a Btrfs or ZFS server. If snapshots exist, the missing space is in them and no pathname-based tool will find it.btrfs filesystem usageorzfs list -o spaceanswer the questiondustructurally cannot. - Assuming
duon a network share is quick. Every stat is a round trip. A tree that takes two seconds locally can take twenty minutes over NFS or SMB. - Comparing
dutotals from two separate runs. Hard links are deduplicated within a run only, so two runs over trees that share files will double-count between them. - Trusting a total taken while something is writing.
duis not atomic. On an active server the number is a smear across the time the walk took. - Freeing space by deleting inside a directory that is mounted over. You are deleting from the mounted filesystem, not the one whose files are hidden underneath.
- Filling a filesystem to exactly 100 percent. Once the root reserve is gone too, services that cannot write their logs or state files fail in confusing ways. Keep headroom.
10. Summary
du and df both shipped on 3 November 1971 and people have been confused by the difference between them ever since. The confusion is worth clearing up once, because it turns a full disk from a mystery into a checklist.
Underneath the whole article is a single idea worth stating plainly. "How big is this?" is not one question. A file has at least five sizes, and they can all be different at once:
apparent size how many bytes it contains ls -l, du --apparent-size
allocated blocks what the filesystem gave it du
physical space after compression or deduplication filesystem-specific tools
shared blocks it holds in common with others reflinks, snapshots
reclaimable what deleting it would actually free none of the above
On an ordinary ext4 file the first two are close and the rest do not arise, which is why the question feels simple most of the time. A sparse image, a snapshotted volume or a hard-linked backup tree pulls them apart, and then the honest answer to "how big is it" is another question: which of those did you mean? du answers the second line, well and quickly, and knowing that is most of what this article is for.
dfasks the filesystem how much room is left.duasks the files how much room they take. Different questions, different answers, and both correct.- Learn one line:
du -h -d 1 DIR | sort -h -r. Descend into the biggest entry and repeat. dureports blocks, not bytes. A one-byte file costs 4 KB, and 100,000 small files can cost 410 MB to hold 10 MB of data.--apparent-sizegives the content instead. On a sparse filelssays 100M,dusays 0, and both are right.- Use
-xnear the root. Without it,du -sh /runon this machine walked into a FUSE mount and had not finished after twenty seconds;du -sxh /runanswered instantly. - Check
df -itoo. Out of inodes looks exactly like out of space, and needs the opposite fix: delete many files, not big ones.du --inodesfinds them. - When the two disagree, the gap is made of four things: the 5 percent root reserve (measured at 5.1 percent here), deleted-but-open files, files hidden under a mount point, and filesystem metadata.
- Deleted-but-open is the big one: 300 MB stayed used after
rmand came back the instant the descriptor closed. Find them withsudo lsof -nP +L1. 2>/dev/nullmakesdulie. Unreadable directories are silently absent from the total, sodu -sh /rootreports 4.0K for a directory with contents.ducounts hard links once and does not double-count overlapping arguments, but only within a single run.- It is slow because it must stat every file: 207,983 files took 0.68s, where
dftakes 0.00s whatever the disk size. - Fix the unit in scripts (
-skor-B M) and parse withcut -f1, because the separator is a tab and the default unit changes underPOSIXLY_CORRECT. - Symlinks are not followed by default, and that is correct: 4.0K for the link against 21M with
-L.-Dfollows only the ones you name. Do not confuse-l(hard links) with-L(symbolic). - Directories cost space too: 4.0K empty, and 164 KB once they hold 3000 names.
- On Btrfs or ZFS, add a fifth cause to the df/du gap: snapshots, reflinks, compression and copy-on-write. No pathname-walking tool can see them; use the filesystem's own tools.
- Write
-d 1, not--max-depth=1. Same result, and BusyBox rejects the long form. - Install
ncdu. One walk, then an interactive tree you can delete from.
This is the quick reference worth keeping:
THE ONE TO REMEMBER
du -h -d 1 DIR | sort -h -r biggest first, one level down
du -sh DIR a single total
du -sh /home/* one total per entry
du -ah DIR | sort -h -r | head the biggest FILES, not directories
CONTROLLING THE WALK
-x stay on one filesystem <- USE THIS near /
-d N only N levels deep
-S each dir WITHOUT its subdirectories
-a include files, not just directories
--exclude=PATTERN skip matching entries -X FILE for a list
-t 1G / -t -1k only bigger / only smaller than
SYMLINKS (the default is right; -l is NOT -L)
-P follow nothing (the default)
-L follow every symlink (can wander far)
-D / -H follow only links named as arguments
-l count HARD links repeatedly (different thing)
WHAT IS BEING COUNTED
du -sh DIR blocks actually allocated (the default)
du -sh --apparent-size DIR bytes the files contain
big gap between them = many small files, or a sparse file
-l count hard links repeatedly (default: once)
--inodes count FILES instead of bytes
-h / --si / -B M / -k 1024 / 1000 / chosen unit / POSIX-safe
WHEN THE DISK IS FULL
df -h which filesystem, how full
df -i or has it run out of inodes?
du -shx /* | sort -h -r top level, then descend
sudo lsof -nP +L1 deleted-but-open files that du CANNOT see
sudo tune2fs -m 1 /dev/X reclaim the root reserve (data volumes only)
IN SCRIPTS
du -sk DIR | cut -f1 fixed unit, tab-separated, space-safe
find ... -print0 | du -sh --files0-from=- -c
2>/dev/null hides permission errors AND quietly lowers the total
PORTABILITY (BusyBox is what you get in a minimal image)
-s -h -a -c -x -k -m -l -L -H -d N GNU and BusyBox both
--max-depth --apparent-size --exclude GNU only (-d and -b are the portable
--inodes -t -S -B --si spellings of the first two)
UNITS: four env vars change a bare du number
DU_BLOCK_SIZE BLOCK_SIZE BLOCKSIZE POSIXLY_CORRECT
always pass -h, -k or -B in a script
df asks the filesystem. du asks the files. The gap is what du cannot see.
On Btrfs/ZFS add snapshots and shared extents, which no path-walker can see.
The next time a disk fills up, the whole job is four commands: df -h to confirm which filesystem, df -i to rule out inodes, then du -h -d 1 -x twice. And if a server keeps filling up and nobody can say what is doing it, the answer is often a file that no longer has a name, which is exactly the thing du was never able to show you.


Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.












