Skip to main content

Linux command: du

03 September 2026

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:

OptionShort forWhat 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:

dudf
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

The right mental model: df asks the filesystem how much room is left. du asks the files how much room they take. When those two disagree, the difference is made of things du cannot see, and section 7.1 is a catalogue of them.

Back to top

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.

Back to top

3. A Short History

Both commands are as old as Unix itself. Not "early Unix": the actual first release.

EraMilestone
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:

OptionGNU coreutilsBusyBox 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.

Back to top

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 top

5. 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.

Back to top

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.

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.

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:

OptionBehaviour
-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.

Back to top

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:

FeatureWhat 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 needReach 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.

Back to top

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 -x whenever you start near the root. Without it du wanders into network shares and FUSE mounts and can hang for good.
  • Check df -h and df -i first. 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 sudo when the answer matters.
  • Fix the unit in scripts. du -sk or du -B M, never a bare du, because the default is 1024 bytes normally and 512 under POSIXLY_CORRECT.
  • Parse with cut -f1, not awk '{print $1}'. The separator is a single tab, and paths contain spaces.
  • Compare du -sh with du -sh --apparent-size when 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 -t to cut the noise rather than piping through head and hoping.
  • When df and du disagree by a lot, look for deleted-but-open files first. sudo lsof -nP +L1, then restart whatever is holding them.
  • Install ncdu on 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 consider ionice -c3.
  • Prefer -d 1 to --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". -L is for "how much is reachable from here" and can wander.
  • On a snapshotting filesystem, reach for its own tools. btrfs filesystem usage and zfs list -o space see what du cannot.
  • 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 top

9. Common Mistakes

9.1 Myth Versus Reality

MythReality
"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 -x on a desktop. It will descend into /run/user/1000/gvfs and whatever that is mounting, and may never come back.
  • Reading du output without sorting it. The order is directory-walk order, not size order. sort -h -r is not optional.
  • Forgetting -h on one side of a pipe. sort -h needs the suffixes that du -h produces. du -k | sort -n also 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, so cut -f1 is correct.
  • Deleting from ncdu without looking. It deletes immediately and permanently, with no trash and no undo.
  • Using -L without thinking about where the links go. A symlink can point outside the tree, onto another filesystem, or back at a parent directory. -L follows all of them, so a scan you expected to take a second can wander a long way.
  • Blaming du on a Btrfs or ZFS server. If snapshots exist, the missing space is in them and no pathname-based tool will find it. btrfs filesystem usage or zfs list -o space answer the question du structurally cannot.
  • Assuming du on 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 du totals 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. du is 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.
Back to top

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.

  • df asks the filesystem how much room is left. du asks 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.
  • du reports 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-size gives the content instead. On a sparse file ls says 100M, du says 0, and both are right.
  • Use -x near the root. Without it, du -sh /run on this machine walked into a FUSE mount and had not finished after twenty seconds; du -sxh /run answered instantly.
  • Check df -i too. Out of inodes looks exactly like out of space, and needs the opposite fix: delete many files, not big ones. du --inodes finds 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 rm and came back the instant the descriptor closed. Find them with sudo lsof -nP +L1.
  • 2>/dev/null makes du lie. Unreadable directories are silently absent from the total, so du -sh /root reports 4.0K for a directory with contents.
  • du counts 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 df takes 0.00s whatever the disk size.
  • Fix the unit in scripts (-sk or -B M) and parse with cut -f1, because the separator is a tab and the default unit changes under POSIXLY_CORRECT.
  • Symlinks are not followed by default, and that is correct: 4.0K for the link against 21M with -L. -D follows 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.

Back to top
Linux command: du
Peter Martin
Peter Martin
Joomla Specialist

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