cd ~/>cat multi-boot-partitioning-btrfs-space-accounting.md

Multi-boot partitioning / btrfs space accounting

#linux#btrfs#disk#docker#partitioning

The discrepancy

df reported the btrfs pool at 70% full. ncdu on the root showed 40% of that. The two numbers disagreed by hundreds of gigabytes. This is not a bug — it is a consequence of how btrfs accounts for space, and understanding it prevents wasted hours of investigation.

Why df shows more than ncdu

Three independent causes, each potentially significant.

1. Block group allocation overhead

btrfs allocates disk space in block groups — typically 1G chunks for data and 256M for metadata. A block group can be partially used. If you have many sparse block groups, btrfs has reserved the space on disk but not filled it. df counts allocated block groups as used. du counts file data only.

btrfs fi usage / shows the breakdown:

btrfs filesystem usage /
Overall:
    Device size:                 887.00GiB
    Device allocated:            612.00GiB
    Device unallocated:          275.00GiB
    Device missing:                  0.00B
    Used:                        480.50GiB
    Free (estimated):            394.22GiB
    Data ratio:                       1.00
    Metadata ratio:                   1.00
    Global reserve:              512.00MiB

Data,single: Size:604.00GiB, Used:478.00GiB
Metadata,single: Size:8.00GiB, Used:2.50GiB
System,single: Size:32.00MiB, Used:16.00KiB

“Device allocated” (612G) is what df uses for its Used figure. “Used” (480G) is what is actually occupied by file data and metadata. The gap is allocated-but-sparse block groups.

2. The Timeshift orphan

The largest single cause of the discrepancy here was a Timeshift snapshot sitting at the pool root, invisible to normal tools.

Timeshift creates btrfs snapshots at subvolid 5 (the btrfs top-level subvolume, the pool root). snapper does not know about Timeshift snapshots. ls / does not show them. ncdu running as a regular user sees only the mounted @void subvolume.

Finding orphaned subvolumes at the pool root requires mounting subvolid=5:

mount -o subvolid=5 /dev/nvme0n1p5 /mnt/pool-root
ls /mnt/pool-root

In this case, that showed a path like 1/snapshot — a 61GB Timeshift snapshot from a previous Arch installation that was never cleaned up. It was not visible to snapper, not visible to ncdu on /, and accounted for a substantial chunk of the discrepancy.

Delete orphaned snapshots by path:

btrfs subvolume delete /mnt/pool-root/1/snapshot

3. Docker’s /var/lib

ncdu without root privileges does not traverse directories it cannot read. Docker’s storage at /var/lib/docker is owned by root and not world-readable. Running ncdu as a regular user silently skips it.

The actual footprint here was 144GB in images and 64GB in build cache:

sudo du -sh /var/lib/docker/
# 208G /var/lib/docker/

docker system df
# Images:      144GB
# Build cache:  64GB

Running ncdu as root (sudo ncdu /) shows the real picture.

btrfs fi df vs btrfs fi usage vs df

These three commands answer different questions.

df -h / — reports from the perspective of the VFS layer: used/available as the kernel presents them. On btrfs, this reflects allocated block groups, not file data. Good for “will I get ENOSPC soon?” but misleading for “how much data do I have?”

df -h /

btrfs fi df / — shows block group allocation by type (Data, Metadata, System) and how much of each is used. Faster than btrfs fi usage but less informative.

btrfs filesystem df /
Data, single: total=604.00GiB, used=478.00GiB
Metadata, single: total=8.00GiB, used=2.50GiB
System, single: total=32.00MiB, used=16.00KiB

btrfs fi usage / — the most complete view. Shows device size, allocated, unallocated, and actual used space. Shows the gap between what is allocated and what is occupied. This is the correct command when diagnosing space issues.

Metadata can fill before data

btrfs metadata and data occupy separate block group pools. It is possible to have data block groups with space available while metadata block groups are 100% full. When metadata is full, the filesystem goes read-only.

The symptom is confusing: df shows available space, writes fail with ENOSPC, and the filesystem is suddenly read-only. Check metadata specifically:

btrfs filesystem df / | grep Metadata

If Metadata is at or near capacity, run a balance to redistribute. See [Multi-boot partitioning / btrfs metadata and balance].

Practical checklist for space investigation

  1. Run btrfs fi usage / — get the real picture of allocated vs used
  2. Mount subvolid=5 and ls the pool root — look for orphaned snapshots
  3. Run sudo ncdu / — get file-level breakdown including root-owned directories
  4. Run docker system df if Docker is in use — images and build cache are often the largest hidden consumer
  5. Check each btrfs group type separately if ENOSPC appears despite available data space
// END OF TRANSMISSION
See you, Space Cowboy.

Connection closed. Returning to terminal...