Btrfs in Production: Snapshots, Send/Receive, Quotas, and the Pitfalls
Btrfs has a strange reputation. It is the default root filesystem on openSUSE and Fedora Workstation. It powers Synology’s BTRFS volume type. It survives in Facebook’s fleet. And yet an entire generation of sysadmins still avoids it, remembers the RAID5/6 warnings from 2017, and tells you to use ZFS or XFS. The truth is more nuanced: Btrfs is excellent at some things, dangerous at others, and the dangerous parts have been clearly documented for years. This post walks through what it actually gets right — snapshots, send/receive, transparent compression, reflinks, subvolume-based layouts — and the pitfalls that earned it its reputation.
The mental model
Btrfs is a copy-on-write B-tree filesystem. Every metadata structure is a B-tree. Every write allocates new blocks rather than overwriting existing ones. This is the source of almost every interesting property Btrfs has, and also the source of its weirder failure modes.
When you write a single byte to a file on ext4 or XFS, the filesystem overwrites the block in place. On Btrfs, it allocates a new block, updates the file’s extent tree to point at the new block, and eventually frees the old block during cleanup. Because the old block is never touched until explicitly freed, snapshots are essentially free — they just pin a pointer to the current state of the B-tree.
This is also why df lies to you, why free space is unpredictable, why you can fill a disk to 60% and still get ENOSPC, and why balance operations exist. Accept that a Btrfs filesystem is not a set of files on blocks — it is a tree of trees.
Subvolume layout: the decision you can’t take back easily
A subvolume is the unit of snapshotting, quotas, and btrfs send. Everything about operating Btrfs revolves around subvolume layout, and choosing a bad one at install time is the single biggest regret people have.
The convention that works for almost everyone:
/mnt/btrfs-root/ # top-level subvolume (id 5)
├── @ # mounted at /
├── @home # mounted at /home
├── @var-log # mounted at /var/log (no snapshots)
├── @var-cache # mounted at /var/cache (no snapshots)
├── @snapshots # mounted at /.snapshots (holds snapshots of @)
└── @home-snapshots # snapshots of @home
The reasoning:
- Separate
@and@homeso you can roll back the OS without nuking user files. - Exclude
/var/logand/var/cachefrom the root subvolume so OS snapshots don’t drag gigabytes of log churn with them. This matters — snapper-style hourly snapshots accumulate fast when you include/var/log. - Keep snapshot storage inside the same filesystem (not in
@) so snapshots are not themselves snapshotted.
Mount the top-level subvolume somewhere (/mnt/btrfs-root via subvolid=5) so you can administer subvolumes directly. You need this for btrfs subvolume delete on snapshots and for cleaning up leftover subvolumes.
Do this at install time. Converting an existing flat Btrfs filesystem into this layout is technically possible via btrfs subvolume snapshot and bind mounts, but it is tedious and error-prone, and every installer-generated layout you inherit will be slightly different.
Snapshots: cheap, but not free
|
|
The -r flag makes it read-only, which is required for btrfs send. Creating a snapshot is O(1) — it just pins the B-tree root. Space usage starts at zero and grows as the original subvolume diverges.
What people miss: snapshots don’t pay for themselves when you hold them alongside workloads that rewrite data heavily. Every block the original workload rewrites becomes a block the snapshot must keep alive. A VM image with frequent internal writes held under a daily snapshot for a month can easily consume 30× its nominal size.
This is why databases on Btrfs are contentious. PostgreSQL’s random writes to shared_buffers, WAL, and data files generate enormous snapshot fragmentation over time. If you must run PostgreSQL on Btrfs:
- Set
chattr +C(nodatacow) on the data directory before any files are created in it — this disables COW and compression for that directory. - Never snapshot the data directory. Snapshotting a nodatacow directory re-enables COW for any blocks shared between the original and the snapshot, destroying the point of nodatacow.
- Run
pg_basebackuporpgBackRestfor backups, not snapshots.
The same applies to any workload with heavy random writes to large files: KVM qcow2 images, MySQL ibdata, Elasticsearch shards.
Snapper and Timeshift
Two tools dominate Btrfs snapshot management:
- Snapper (openSUSE’s tool, now standard on Fedora via
btrfs-assistant): configuration-based, handles pre/post pacman/dnf/zypper snapshots via hooks, cleans up on a timeline policy (hourly/daily/weekly/monthly). - Timeshift: simpler, GUI-forward, designed around system-level rollback.
Snapper’s timeline config is where most disk-space surprises come from. The defaults keep 10 hourly + 10 daily + 10 weekly + 10 monthly + 10 yearly — that’s 50 snapshots, and for a desktop doing package updates that can reach tens of gigabytes in a few weeks. Trim aggressively:
|
|
Set NUMBER_LIMIT and cleanup algorithm (NUMBER_CLEANUP="yes") so that pre/post pairs get pruned too — otherwise your dnf upgrade snapshots pile up forever.
Send/receive: the killer feature
btrfs send produces a stream describing the difference between two read-only snapshots. btrfs receive applies that stream to a target filesystem, reconstructing the snapshot identically.
Basic flow:
|
|
The -p flag specifies the parent — the previously-sent snapshot that both sides already have. send emits only the diff. On a 2 TB home directory with a day of normal activity, the incremental stream is typically a few hundred MB.
Over the network, pipe through SSH:
|
|
Things send/receive actually gives you
- Block-level deduplication at the protocol level — unchanged extents aren’t retransmitted.
- Preserves all Btrfs metadata — permissions, xattrs, compression flags, reflinks.
- No interpretation of files — the stream is opaque to rsync-style logic. A 100 GB sparse file stays sparse.
- Atomic — the receiving side doesn’t expose the new snapshot until the stream completes.
Things send/receive does NOT give you
- Cross-filesystem-UUID guarantees. If you recreate the source filesystem, your parent snapshot on the destination becomes useless. Plan for this.
- Resume. If an SSH connection drops mid-send, you start over. Wrap in
btrbkorsnapraid-btrfsfor retry logic. (btrfs send --no-dataplusmbufferhelps with stalls but not disconnections.) - Encryption. The stream is plaintext. Use an encrypted destination filesystem or tunnel over SSH.
The standard wrapper is btrbk, which handles snapshot rotation, sending, retention policies on both ends, and retries. Its config file is the clean way to run send/receive in production:
transaction_log /var/log/btrbk.log
snapshot_preserve_min 2d
snapshot_preserve 14d 8w 6m
target_preserve_min no
target_preserve 20d 10w 12m
ssh_identity /etc/btrbk/ssh/id_ed25519
volume /mnt/btrfs
subvolume @home
target send-receive ssh://backup.example.com/mnt/backup/@home
Run from cron or a systemd timer. One of the cleanest backup tools in the Linux ecosystem.
Transparent compression
Btrfs can compress extents transparently with zstd, lzo, or zlib. For almost any workload, zstd level 3 is the right default:
|
|
Or per-file via chattr +c / btrfs property set <path> compression zstd. Compression is decided per-extent at write time — existing data is not recompressed unless you run btrfs filesystem defragment -r -czstd <path>.
When to tune the level:
- zstd:1 — backup targets, large capacities, CPU-constrained NAS hardware.
- zstd:3 (default) — general-purpose, good balance.
- zstd:9+ — archival data written once, read rarely. Compression is expensive but decompression stays fast.
Real-world ratios on mixed workloads land between 1.3× and 2.0×. Source code trees, logs, and text documents compress well; photos, videos, and already-compressed archives do not — and Btrfs is smart enough to detect incompressible extents and store them raw.
Check compression ratios with compsize (a separate utility):
|
|
Quotas (qgroups): here be dragons
Btrfs quotas are subvolume-level and recursive. They sound great, they look great in the docs, and they have caused so many production incidents that the Btrfs maintainers will tell you to avoid them unless you truly need them.
The mechanism: every extent tracks which qgroup owns it. When a snapshot is created, extent ownership is shared across qgroups. When it’s deleted, accounting has to update across all sharing qgroups. Balance operations have to rewrite quota state. The result is that enabling quota enable on a large filesystem with many subvolumes can:
- Slow metadata operations by 10× or more.
- Cause balance operations to take days instead of hours.
- Produce incorrect numbers that require
btrfs quota rescan. - Occasionally deadlock on very old kernels (fixed, but the reputation lingers).
If you need per-user reporting without hard enforcement, consider running du -s on a cron job and accepting the inaccuracy. If you need hard enforcement, use separate Btrfs filesystems per tenant — the overhead of multiple filesystems is less than the overhead of qgroups at scale.
If you do enable them:
|
|
And be aware that referenced vs exclusive size means something specific: referenced counts every extent the qgroup points at, exclusive counts extents only reachable through this qgroup. Snapshots inflate referenced and deflate exclusive. Most monitoring dashboards get this wrong.
Reflinks: the other reason to use Btrfs
cp --reflink=always <src> <dst> creates a new file that shares all extents with the source until either side is modified. This is the same mechanism as snapshots but at file granularity. Use cases:
- Virtual machine cloning. Copy a base image as a reflink; boot a fresh VM in milliseconds using zero extra space.
- Build systems. Incremental builds that copy artifact trees become instant.
- Container images. Podman with the
overlaydriver can use reflinks on Btrfs to make image pulls near-instant after the first layer. - Backup staging. Reflink a tree before mutating it to get a cheap atomic snapshot of a single directory.
Most modern coreutils default to --reflink=auto, so cp automatically uses reflinks on Btrfs. Verify with btrfs filesystem du:
|
|
The pitfalls that earned the reputation
RAID5/RAID6
Do not use Btrfs native RAID5 or RAID6. The parity write hole is unfixed. The project’s own wiki has had a warning banner for over a decade. Use mdadm RAID5/6 underneath Btrfs, or use RAID1/RAID1C3/RAID10 for Btrfs-native redundancy.
For mirrors, Btrfs native RAID is genuinely excellent — self-healing scrubs catch bitrot, and you can add and remove devices online.
ENOSPC when the disk is not full
Btrfs separates metadata and data into different chunk types. You can exhaust metadata chunks while data chunks have terabytes free. btrfs fi df shows the split:
|
|
When metadata is near full, the fix is a balance with a filter:
|
|
This rewrites metadata chunks that are less than 50% used, reclaiming the empty space. If balance itself fails with ENOSPC (a classic Btrfs catch-22), add a temporary device — even a USB stick — long enough to complete the balance, then remove it.
Fragmentation on COW workloads
VM images, database files, and torrent downloads generate pathological fragmentation. Symptoms: slow reads, high CPU during access, disks that sound like coffee grinders. Fixes in order of severity:
chattr +Con the directory before creating the files to disable COW.- Mount with
autodefragfor home-directory-like workloads (generates extra I/O, not recommended for SSDs with heavy writes). - Manually
btrfs filesystem defragment -r -czstd <path>on affected trees. Note that defragment breaks reflinks — it rewrites extents.
The “device full” during balance trap
Running btrfs balance start / with no filters on a nearly-full filesystem can fail partway through and leave the filesystem in a worse state than before. Always use filters:
|
|
Monitor with btrfs balance status / in another terminal. Cancel with btrfs balance cancel / if it stalls; the partial work is durable.
Old kernels
Btrfs fixes land constantly. If you are running RHEL 7 (kernel 3.10) or Ubuntu 16.04 (4.4), you are running a filesystem with hundreds of known bugs fixed upstream. Real production use needs at least a 6.1 LTS kernel; 6.6+ is better. This is the single biggest variable in “Btrfs broke” stories — someone is almost always running an ancient kernel.
Day-2 operations checklist
- Weekly scrub:
systemctl enable --now btrfs-scrub@-.timer(on most distros) — reads every block, verifies checksums, repairs from mirror copy if available. - Monthly balance:
btrfs balance start -dusage=50 -musage=50 /to reclaim partially-used chunks. - Monitor free space the Btrfs way:
btrfs filesystem usage /tells the truth;dfdoes not. - Watch for checksum errors:
dmesg | grep -i btrfsshould be silent. Anychecksum errormessages mean a disk is failing or has already failed. - Keep scrubs fast: scrub speed is usually disk-bound; for large pools, schedule during off-hours.
- Test restores: a backup you haven’t restored is a hope, not a backup.
btrfs receiveinto a scratch partition monthly.
When Btrfs is the right answer
- Rootfs with package-manager integration (openSUSE, Fedora): snapshots on upgrade give you a working rollback if a kernel update bricks the machine.
- Home directory backup source: send/receive is faster and more accurate than rsync for TB-scale personal data.
- Container host storage: reflinks make image and volume operations cheap.
- NAS bulk storage on top of mdadm RAID: compression and checksumming catch bitrot on large warm storage.
When to pick something else
- PostgreSQL or MySQL primaries: use ext4 or XFS and let the database handle its own data integrity.
- High-churn VM hosts without nodatacow discipline: ZFS or LVM+ext4 is less finicky.
- RAID5/RAID6 natively: use ZFS, or mdadm underneath ext4/XFS.
- Teams without time to learn the operational quirks: ext4 and XFS still ship and still work.
Btrfs rewards operators who read the manual and penalizes those who don’t. The maintainers have been extraordinarily transparent about its limitations — more so than most filesystems — and if you stay inside the supported envelope (modern kernel, RAID1 or RAID10 only, disciplined nodatacow on DB files, aggressive snapshot retention policies, regular scrub and balance), it is genuinely one of the most capable filesystems available on Linux today.
Comments