Why du Overcounts On APFS

3 min readApple, Xcode & Tools

Jake Marsh

Chrome keeps copies of itself in a temp folder on my Mac, 14 of them right now, and du says they take up 20 GB:

Shell
$ du -sh /var/folders/*/*/X/com.google.Chrome.code_sign_clone
 20G	/var/folders/l6/1y30n0fd2lg5fymm_086lcyh0000gn/X/com.google.Chrome.code_sign_clone

Deleting them would free far less than that. They’re APFS clones, and nearly all of their blocks are shared with the copy of Chrome in /Applications. du adds up file sizes and has no idea two files can point at the same storage, so every clone gets counted in full.

I learned this the expensive way while clearing space. I emptied a Trash that Finder said held 48.6 GB, and the free space on the disk went up by about 6 GB.

What A Clone Is#

When you copy a file on APFS with cp -c, or with Finder, or with anything that calls clonefile(), the new file is a clone. It gets its own name and metadata but points at the original’s blocks. Nothing is written until one of the two files changes, and then only the changed blocks get new storage.

Here’s a 256 MB file of random data and three clones of it:

Shell
$ dd if=/dev/urandom of=original.bin bs=1m count=256
$ cp -c original.bin copy1.bin
$ cp -c original.bin copy2.bin
$ cp -c original.bin copy3.bin
$ du -sh .
1.0G	.

The three clones changed the disk’s free space by 92 KB. du reports 1.0 GB for the folder. A plain cp of the same file, for comparison, took 257 MB.

Checking Whether Two Files Share Storage#

fcntl with F_LOG2PHYS_EXT asks the file system where a byte of a file lives on the device. Clones of the same file give the same answer:

Swift
import Darwin

// Prints where the first byte of each file lives on disk.
// Two files with the same device offset share storage (APFS clones).
func physicalOffset(_ path: String) -> Int64? {
    let fd = open(path, O_RDONLY)
    guard fd >= 0 else { return nil }
    defer { close(fd) }
    var l2p = log2phys(l2p_flags: 0, l2p_contigbytes: 1, l2p_devoffset: 0)
    guard fcntl(fd, F_LOG2PHYS_EXT, &l2p) != -1 else { return nil }
    return l2p.l2p_devoffset
}

for path in CommandLine.arguments.dropFirst() {
    if let offset = physicalOffset(path) {
        print("\(offset)\t\(path)")
    } else {
        print("?\t\(path)  (\(String(cString: strerror(errno))))")
    }
}

Save it as shared-blocks.swift and run it on the files you want to compare:

Shell
$ swift shared-blocks.swift original.bin copy1.bin copy2.bin copy3.bin plaincopy.bin
955166838784	original.bin
955166838784	copy1.bin
955166838784	copy2.bin
955166838784	copy3.bin
852936241152	plaincopy.bin

The clones all start at the same place on the disk, and the plain copy has its own.

It only checks the first byte, which is enough to tell a clone from a copy. A clone that has since been edited can share some blocks and not others, and this won’t show you how many.

Where Clones Pile Up#

The Chrome folder is the biggest one on my Mac. Each of those copies holds Google Chrome Framework, a 526 MB binary, and there are 28 of them across the 14 folders. The installed Chrome holds two versions of the framework, 156.0.8078.74 and 156.0.8078.80. Run the checker on all 28 copies and 27 of them share storage with one of those two. The last one is a clone of an older version.

node_modules folders are the other big one. bun install uses clonefile on macOS by default (--backend lists it as the default), so the same package in several projects often shares one copy. Checking typescript.js, a 9 MB file, in a few of my projects:

Shell
853964869632	2025-06-mcp-apple-mail/node_modules/typescript/lib/typescript.js
853964869632	2025-06-mcp-simple-email/node_modules/typescript/lib/typescript.js
853964869632	2025-06-namecheap-mcp-server/node_modules/typescript/lib/typescript.js
153033752576	agentenv/node_modules/typescript/lib/typescript.js
153033752576	agentic-sales-app/node_modules/typescript/lib/typescript.js
445363130368	agentic-todo/node_modules/typescript/lib/typescript.js

The first three share one copy of TypeScript 5.8.3, and the next two share one copy of 5.9.3. The last project also has 5.9.3 but its own copy, so the same version number doesn’t guarantee the files are shared.

When you want to know what deleting something will free, check df before and after. That’s the only number that counts clones correctly.

Keep Disk Scans Out Of iCloud Drive#

While I was hunting for space I ran into a second problem. My free space kept dropping by 10 to 30 GB and then coming back on its own a while later.

The cause was my own scans. iCloud Drive lives at ~/Library/Mobile Documents, and with Optimize Mac Storage turned on, macOS evicts files you haven’t opened in a while and keeps only a placeholder. Walking into those folders with du or find made macOS download evicted files again. Most of what came back down was Logic Pro projects of 30 to 150 MB each, and iCloud downloads a package like that whole. One du of iCloud Drive took it from 15.9 GB on disk to 26.4 GB, and a find /Users I left running pulled files down at about 1.1 GB a minute. macOS evicts them again later, which is why the space seemed to come back by itself.

Anything that walks your home folder or ~/Library will go in there unless you tell it not to. Both tools can skip it:

Shell
du -sh -I "Mobile Documents" ~/Library
find ~ -path "*/Mobile Documents" -prune -o -name "*.log" -print

If your disk is filling up and you suspect iCloud is downloading, brctl status com.apple.CloudDocs shows what’s queued.