Disk space on Linux: df, du and free, and why btrfs lies

See which disk is full, find what filled it, and understand why df, du and btrfs disagree.

Part 8 of 12 in Linux basics for self-hosters. Last updated

In short: df -h shows how full each mounted file system is; du -xh --max-depth=1 / | sort -h shows which directories use the space; free -h shows memory, where the available column is the one that matters. When df says full and du finds nothing, look for deleted files a program still holds open (sudo lsof +L1) and, on openSUSE's btrfs, for snapshots (sudo snapper list, sudo btrfs filesystem usage /).

df: how full is each disk?

df -h                              # every mounted file system, human units
df -h /srv                         # only the one that holds /srv
df -hT -x tmpfs -x devtmpfs        # with file system type, without memory file systems
df -i                              # inodes: the number of files, not bytes
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  233G  201G   30G  88% /
/dev/sda1       3.6T  2.9T  700G  81% /srv/media

Read the Mounted on column first: it tells you which directory lives on which disk. Use% above 90 on / deserves attention, because services start failing in odd ways when the root file system is full. On ext4, Used plus Avail is a little less than Size: 5 % is reserved for root by default. On btrfs, df -i shows zeros, because btrfs creates inodes as needed. Paste your output into the df, du and free explainer for a line-by-line reading.

A real openSUSE laptop with the default btrfs layout. / and /home show the same device and the same numbers, because they are subvolumes of one btrfs pool, not two partitions. /tmp is a tmpfs in memory:

$ df -hT / /home /tmp
Filesystem              Type   Size  Used Avail Use% Mounted on
/dev/mapper/system-root btrfs  435G  311G  123G  72% /
/dev/mapper/system-root btrfs  435G  311G  123G  72% /home
tmpfs                   tmpfs   16G  3.1G   13G  20% /tmp

Real output on openSUSE Tumbleweed, 2026-09-28; user and host names and addresses redacted.

du: what is using the space?

du -sh ~/Downloads                          # one total
du -h --max-depth=1 /var | sort -h          # subfolders, largest last
sudo du -xh --max-depth=1 / | sort -rh | head -15   # the whole root disk, largest first

-x keeps du on one file system, so a mounted NAS share or /proc does not end up in the total. Walk down: run it on /, then on the biggest directory it shows, and so on. To find single large files instead, use sudo find / -xdev -type f -size +500M -exec ls -lh {} +, explained in find and grep recipes.

free: is memory really low?

free -h
               total        used        free      shared  buff/cache   available
Mem:            15Gi       6.1Gi       1.2Gi       310Mi       8.3Gi       9.0Gi
Swap:          2.0Gi          0B       2.0Gi

Look at available, not free. Linux fills idle memory with disk cache (buff/cache) and gives it back the moment a program needs it. A server with 1.2 GiB free and 9 GiB available is fine. Memory is really short when available stays low and swap used keeps growing. See free(1).

When df and du disagree

df counts blocks the file system has in use; du adds up the files it can see. Three things make them differ:

  1. Deleted files that are still open. Delete a 20 GB log while a service writes to it, and the name is gone but the space is not. sudo lsof +L1 lists those files and the process holding each one; restarting that service frees the space.
  2. Files under a mount point. Files written to /mnt/nas while the share was not mounted are hidden once it is mounted again. Unmount it and look, or bind-mount / somewhere else and run du there.
  3. Snapshots. On btrfs, data kept alive by a snapshot is used space, but du only sees the current files. See the next section.
df counts every used block; du only the files it can walk to.

The first case, reproduced with a 200 MiB file held open by a sleep process: after rm, du no longer counts it, but lsof shows the space is still taken, marked (deleted). Stopping the process frees it.

$ head -c 200M /dev/zero > held.bin
$ sleep 600 < held.bin &
$ du -sh .
200M	.
$ rm held.bin
$ du -sh .
0	.
$ lsof -nP +L1 | grep held.bin
sleep     3099381  user    0r   REG   0,47  209715200     0 32734162 /home/user/scratch/held.bin (deleted)
$ kill %1

Real output on openSUSE Tumbleweed, 2026-09-28; user and host names and addresses redacted.

btrfs and snapshots on openSUSE

openSUSE installs its root file system on btrfs by default, and snapper takes a snapshot before and after every zypper run. That is what makes rollbacks possible, and it is also where "lost" space goes: when an update replaces 2 GB of packages, the old versions stay on disk inside the snapshot until it is deleted.

sudo btrfs filesystem usage /     # the real picture: data, metadata, unallocated
sudo snapper list                 # snapshots with number, date and description
sudo snapper delete 412           # delete one snapshot (or a range: 400-410)
sudo snapper cleanup number       # apply the configured cleanup rules now

btrfs lives in /usr/sbin, so run it with sudo. Snapper deletes old snapshots on a timer following the limits in /etc/snapper/configs/root; if the disk is small, lower NUMBER_LIMIT there rather than deleting by hand every week. Keep the most recent snapshots: they are your way back from a broken update.

The config file is readable without sudo. These are the limits on a Tumbleweed install: between 2 and 10 numbered (zypper) snapshots and 4 to 10 important ones, snapshots may use up to half the file system, and hourly timeline snapshots are off:

$ grep -E '^(NUMBER_LIMIT|NUMBER_LIMIT_IMPORTANT|TIMELINE_CREATE|SPACE_LIMIT)=' /etc/snapper/configs/root
SPACE_LIMIT="0.5"
NUMBER_LIMIT="2-10"
NUMBER_LIMIT_IMPORTANT="4-10"
TIMELINE_CREATE="no"

Real output on openSUSE Tumbleweed, 2026-09-28; user and host names and addresses redacted.

Safe places to reclaim space

Common space users on a home server and how to trim them
WhatCheckTrim
Journaljournalctl --disk-usagesudo journalctl --vacuum-size=500M
Package cachesudo du -sh /var/cache/zyppsudo zypper clean -a, sudo dnf clean all, sudo apt clean
Container imagespodman system dfpodman image prune (add -a for every unused image)
btrfs snapshotssudo snapper listsudo snapper delete NUMBER
Your own downloadsdu -h --max-depth=1 ~ | sort -hBy hand, after looking

Rootless and rootful podman have separate storage, so run podman system df both as your user and with sudo. For more space, a NAS share is often cheaper than a new disk; the fstab generator for NAS shares writes the mount line.

Common mistakes

Deleting a log file a service still writes to

The space stays in use until the service restarts. Empty the file in place instead with sudo truncate -s 0 /var/log/big.log, or better, let the journal or logrotate manage it.

Deleting everything in /var/log or /var/lib

/var/lib holds databases, container storage and package manager state. Removing things there by size breaks services and updates. Find the owner first (rpm -qf or dpkg -S), then use that program's own cleanup.

Reading "free" in free -h as the memory left

Low free memory is normal and good: it means the cache is doing its job. Only a low available value, together with growing swap use, means the machine needs more memory or fewer services.

Frequently asked questions

Why does df show the disk as full when du shows much less?

Usually a program still has a deleted file open: the space is only freed when the program closes it. Find it with sudo lsof +L1 and restart that service. Other causes are files hidden under a mount point and, on btrfs, snapshots that du does not count.

What does the available column in free mean?

available is an estimate of how much memory new programs can get without swapping, including cache the kernel can drop. free is memory nobody uses at all, which is small on a healthy system because Linux uses idle memory as disk cache.

How do I see the size of a folder in Linux?

du -sh /path prints the total in human units. du -h --max-depth=1 /path | sort -h lists the subfolders from small to large. Add sudo for system folders, and -x to stay on one file system.

How do I free space on openSUSE btrfs?

Old snapper snapshots are the usual cause. List them with sudo snapper list, delete ones you do not need with sudo snapper delete NUMBER, and check the real usage with sudo btrfs filesystem usage /. The package cache (sudo zypper clean -a) and the journal (sudo journalctl --vacuum-size=500M) are the next places to look.