Btrfs is the copy-on-write filesystem shipped by default on openSUSE/Tumbleweed, Fedora Workstation, and the Synology platform. Its snapshot model is similar to ZFS but its operational surface is different, and the btrfs send/btrfs receive pair is how you turn a snapshot into a portable stream that lands on another btrfs filesystem. This article covers the wire-up, the read-only-subvolume requirement that trips up new operators, and the incremental pattern we use on the engagements where btrfs is the filesystem.
How to verify
A working btrfs replication setup looks like:
btrfs --version
btrfs subvolume list -t /
btrfs subvolume list -ro /mnt/snapshots
btrfs property get -ts /mnt/snapshots/snap-2026-06-01
btrfs send /mnt/snapshots/snap-2026-06-01 | ssh backup btrfs receive /mnt/replica/
The -r flag on subvolume list and the ro=true property are mandatory — btrfs send refuses to operate on writable subvolumes. The receiver lands the stream as a read-only subvolume on the destination.
What’s happening
A btrfs subvolume is a writable filesystem tree with its own inode namespace. A snapshot is a copy-on-write copy of a subvolume — initially identical, then diverging as one side is written to. Snapshots taken with btrfs subvolume snapshot -r are read-only; this is the form send requires.
btrfs send <snapshot> walks the subvolume’s b-tree and emits a stream describing every file, every extent reference, every metadata change. It is conceptually similar to zfs send but the wire format is btrfs-specific.
Incremental sends use a “parent” snapshot the destination already has: btrfs send -p <parent-snap> <new-snap> emits only the delta. The receiver applies it against its local copy of <parent-snap> and ends up with <new-snap>. If the parent has been deleted or renamed on either side, the chain breaks and you need a fresh full.
The biggest operational divergence from ZFS: btrfs subvolumes are not as cleanly hierarchical. A snapshot’s identity is its UUID (btrfs subvolume show), and the parent-uuid tracking is what makes incrementals work. Lose the UUID match (by renaming, recreating, or rebooting into a fresh subvolume) and the chain breaks silently — the send still works but no incremental can build on it.
The procedure
-
Take a read-only snapshot of the source subvolume.
btrfs subvolume snapshot -r /srv/data /mnt/snapshots/data-2026-06-01 -
Send the first full to the destination. The destination must be a btrfs filesystem with a directory ready to receive subvolumes.
ssh backup btrfs subvolume create /mnt/replica/data 2>/dev/null || true btrfs send /mnt/snapshots/data-2026-06-01 | \ ssh backup btrfs receive /mnt/replica/ ssh backup btrfs subvolume list -ro /mnt/replica -
Take the next snapshot and send the incremental.
btrfs subvolume snapshot -r /srv/data /mnt/snapshots/data-2026-06-02 btrfs send -p /mnt/snapshots/data-2026-06-01 /mnt/snapshots/data-2026-06-02 | \ ssh backup btrfs receive /mnt/replica/ -
Verify the UUIDs match.
btrfs subvolume show /mnt/snapshots/data-2026-06-02 | grep -E 'UUID|Parent' ssh backup btrfs subvolume show /mnt/replica/data-2026-06-02 | grep -E 'UUID|Parent'received_uuidon the destination should equal the source’sUUID. -
Wrap it in a script with retention logic. The pattern we use:
#!/usr/bin/env bash set -euo pipefail src=/srv/data snapdir=/mnt/snapshots dest=backup:/mnt/replica ts=$(date +%Y%m%dT%H%M%S) prev=$(ls -1 $snapdir/ | grep "^$(basename $src)-" | tail -1 || true) new="$(basename $src)-$ts" btrfs subvolume snapshot -r $src $snapdir/$new if [[ -n "$prev" ]]; then btrfs send -p $snapdir/$prev $snapdir/$new | ssh ${dest%%:*} btrfs receive ${dest##*:}/ else btrfs send $snapdir/$new | ssh ${dest%%:*} btrfs receive ${dest##*:}/ fi -
Rotate snapshots on both sides.
snapperandbtrbkare the two common tools; for production engagements we lean onbtrbkbecause its config-driven model fits with the rest of our automation.
Operational notes
btrfs sendrequires the snapshot to be read-only. The error message when you forget is “ERROR: subvolume X is not read-only” — clear, but new operators still miss it.- Stream resumption is fragile. If
btrfs send | ssh | btrfs receiveis interrupted, the receiver leaves a partial subvolume that must be deleted before retry.btrbkhandles this; hand-rolled scripts often don’t. - The btrfs filesystem on the destination must be at least as recent as the source. Older kernels reject stream features they don’t recognize. We pin kernels on both sides through configuration management.
btrfs receivedoes not preservechattr +ior some xattrs across versions. Verify any property your workload depends on after the first full.- Quotas (
btrfs qgroup) interact badly with very-many-snapshot scenarios. We disable qgroups on the backup target unless absolutely needed; balance and dedup operations can take hours otherwise. btrfs scrubshould run weekly on both source and destination. Btrfs’s CoW is detection-only without scrub — bit rot remains invisible until you try to read the bad block.
In the engagements we run, btrfs send/receive is the choice on Synology DSM and openSUSE-based stacks where btrfs is already the filesystem. The replication semantics are similar to ZFS but the day-to-day operational surface is more fragile, so we wrap it with btrbk and pair it with a third copy via restic or BorgBackup for a format-portable offsite tier. The way that fits into a broader operating model is at /en/services/managed-operations/.