Baobaobaolin.com
date
entry
048
topic
infrastructure
rev
—

Finding what ate the disk: files modified since yesterday add up to 505 GB, more than the whole APFS container

With 2 GB of disk left, I wanted to know what had been written since yesterday, so I added up the sizes of files modified in that period: 505 GB, more than the whole APFS container. On macOS, file sizes, modification times and du each have a way of overcounting.

When a disk is nearly full, a common approach is to find recently modified files, add up their sizes, and see what grew. Several jobs were running on this Mac at once, free space was down to about 2 GB around noon on October 15, and that's what I did.

The result was 505.23 GB, more than the whole APFS container. This note is about why that number exceeds the container's capacity, and how, when looking for what ate the space on macOS, file sizes, modification times and du each overcount. In order: where 505 GB came from, why the 30 GB from allocated blocks is still wrong, what du overcounts with APFS clones, and the snapshot subtraction I use now. Reading free space itself is covered in the note on df's Capacity.

Where 505 GB came from

The core of the command was find ~ -xdev -newermt '2026-10-14 00:00' -type f (the actual run also used -prune to skip a few mail, messages and photos directories), summing each file's stat -f %z: 46,888 files, 505.23 GB. diskutil info / says the whole APFS container is 494,384,795,648 bytes, about 494.38 GB.

The largest single file exceeds the container on its own: OrbStack's (a tool for running Linux containers) virtual disk data.img.raw, whose stat size is 494,385,775,104 bytes, about 1 MB more than the container. It's a sparse file: its size is the length it claims, and ranges never written take no space. The same file's st_blocks is 38,006,824 blocks of 512 bytes, so 19.46 GB actually allocated; du -k reports 19,003,412 KiB for it, the same number.

So the first overcount comes from %z. To estimate the space files take, use allocated blocks (stat -f %b times 512, or du); hard links, blocks shared by clones and snapshots need separate thought. Around 12:55 I re-ran stat over the same list and summed allocated blocks: 30.42 GB (31 of the files were already gone by then).

The 30 GB isn't this period's growth either

30.42 GB can't be read as what was added in the period either. -newermt filters by modification time (mtime), and an mtime only says it falls after the cutoff: usually the file was written, but not how much, and mtimes can be set directly; this list even contains a file with an mtime in 2082.

The virtual disk is the example: its mtime fell after this list's cutoff, so all 19.46 GB of its allocation was counted as "modified since yesterday", even if only a small part was written that day. Codex's thread_history_1.sqlite is the same: once the main database file's mtime changes, all 2.69 GB is on the list (in a WAL database, new data may go to the -wal file first and reach the main file only at checkpoint). Those two files alone account for over twenty GB of the list (one by allocation, one by file size). The list answers "whose recorded mtime is later than the cutoff", not "how much space was added in the period".

du overcounts APFS clones

Even with du, totals can run high. APFS supports clones: within one APFS volume, cp -c tries to make a clone, which shares data blocks with the original, and only changed parts get new blocks. In a scratch directory I tested three times with a 100 MiB random file, reading Container Free Space from diskutil info / before and after each cp:

cp -c a b   → free space down 37 KB, 8 KB, −4 KB (three runs)
cp a c      → free space down 105 MB, 107 MB, 106 MB
du -k a b c → 102400, 102400, 102400 (KiB)

The clone takes almost no new space, yet du counts each of the three files at 100 MiB, 300 MiB in total; counting shared blocks once, the data is about 200 MiB. Other jobs were writing during the measurement, so the free-space numbers include that background activity, and I didn't measure an error bound. I haven't found a cheap way to see how many files on this machine are clones still sharing blocks, so all I can say is that when one total includes several files sharing blocks, du exceeds the allocation with shared blocks counted once.

What I do now: subtract two snapshots

To answer "what grew during this period", I now take du at two points in time and subtract, while recording df's free space:

du -x -k -d 3 ~ > snap1.txt     # 12:41, about 2 minutes to run
… some time later …
du -x -k -d 3 ~ > snap3.txt     # 13:08
# align by path and compute each directory's difference

-x stays on one file system, -d 3 lists only three levels below home, and -k uses KiB. Results between the two:

  • Home directory total: 297,437,800 → 297,420,956 KiB, a net change of −16.4 MiB.
  • Among home's immediate subdirectories, the biggest growth was ~/.codex, +93.9 MiB, mostly session files and generated images newly written by another job (see 044); next was ~/.claude, +28.1 MiB.
  • Among third-level directories, the biggest shrink was ~/Library/Application Support/Google, −302.3 MiB; checked separately with du -sh around 12:30 and 13:10, Chrome's own profile directory went from 3.6G to 3.3G, for reasons I didn't look into.
  • Comparing df -k taken before each du run, free space went from 1,976,572 KiB to 2,152,656 KiB, up about 172 MiB.

The last two don't reconcile: home shrank by only 16 MiB, yet free space grew by 172 MiB, leaving about 156 MiB unexplained. It could be changes outside home, such as other jobs clearing their own temp files in /private/tmp, or snapshots, clones, or space APFS released on its own; a du pass also takes two minutes, so it isn't a single instant. I didn't measure outside home in these two snapshots, so I don't know which. That's the limit of the method: du sees only where you point it, df sees the whole volume but doesn't say who. Recording both at least tells you how much is still unexplained.

If you remember one thing

When looking for what ate the disk, don't sum the sizes of recently modified files: a sparse file's size can exceed the whole APFS container, and a modification time isn't new data. Take du at two points in time and subtract, record df alongside, and whatever doesn't reconcile is what you haven't explained yet; when one total includes APFS clones sharing blocks, du runs high.

Revision history

  1. First published