How to back up with Btrfs snapshots
A Btrfs snapshot is an instant, read-only copy of a subvolume, but it shares its blocks with the live data on the same filesystem, so it only becomes a backup once it is copied somewhere else. Take one with btrfs subvolume snapshot -r /srv/www /srv/snapshots/www.2026-10-04, copy it with btrfs send /srv/snapshots/www.2026-10-04 | ssh [email protected] btrfs receive /backup/web-01, and from then on send only the changes with btrfs send -p.
Subvolumes and snapshots
Btrfs snapshots work on subvolumes: parts of the filesystem that look like directories but have their own file tree. A snapshot is a subvolume that starts with the same content and shares every block with the original until one of them changes. The examples use a Btrfs filesystem at /srv with a website in the subvolume /srv/www:
findmnt -n -o FSTYPE /srvsudo btrfs subvolume list /srvThe first should print btrfs; elsewhere, btrfs subvolume commands stop with ERROR: Not a Btrfs filesystem: Invalid argument. A plain directory gets ERROR: Not a Btrfs subvolume: Invalid argument: create a subvolume with sudo btrfs subvolume create /srv/www and move the data into it at a quiet moment.
Snapshots are not recursive: a subvolume nested inside the one you snapshot appears in the snapshot as an empty directory. sudo btrfs subvolume list -o /srv/www lists those directly below it; snapshot and send each separately.
Take a read-only snapshot
sudo mkdir -p /srv/snapshotssudo btrfs subvolume snapshot -r /srv/www /srv/snapshots/www.$(date +%F)-r makes it read-only: snapshots are writable by default, but only read-only ones can be sent. A sortable date keeps them in order. Btrfs flushes the subvolume's pending writes and then copies one tree root, so the snapshot is near-instant. Like a power cut, it catches files mid-write; for a database, use the locking steps in the LVM snapshot guide.
A snapshot is an ordinary read-only directory, so restoring one file is a copy:
sudo cp -a /srv/snapshots/www.2026-10-04/html/wp-config.php /srv/www/html/Why a snapshot on the same filesystem is not a backup
The btrfs manual says it plainly: "A snapshot is not a backup". A snapshot shares its blocks with the original, so a bad sector, a failed disk, a damaged filesystem or a deleted server takes both. Snapshots cover deleted files and bad deploys; for the rest, copy them to another machine, ideally in another place (the 3-2-1 rule, snapshots versus backups).
Send the first copy to another server
btrfs send turns a read-only snapshot into a stream, and btrfs receive rebuilds it as a read-only subvolume on another Btrfs filesystem, on a second disk or over SSH. Both need root. On the backup host, mount a Btrfs filesystem at /backup at its top-level subvolume (receive fails if the default subvolume was changed) and make a directory for this server:
mkdir -p /backup/web-01On the server, as root, send the first snapshot in full:
btrfs send /srv/snapshots/www.2026-10-04 | ssh [email protected] btrfs receive /backup/web-01The backup host creates /backup/web-01/www.2026-10-04 and makes it read-only when the stream ends. Check it there:
btrfs subvolume show /backup/web-01/www.2026-10-04A complete copy shows Flags: readonly and a Received UUID equal to the UUID that the same command prints for the snapshot on the server; incremental sends rely on that link. With Linux 6.0 or later on the sender and btrfs-progs 6.0 or later on both sides, --compressed-data sends compressed data without unpacking it.
Pulling is safer: run ssh [email protected] btrfs send /srv/snapshots/www.2026-10-04 | btrfs receive /backup/web-01 on the backup host, and the server holds no login to its backups (protecting backups from ransomware). Receive only trusted streams into a directory other users can't write: btrfs-receive(8) warns that a crafted stream can reflink other files on that filesystem, and that writes into the target during a receive end up in the copy.
Send only the changes with -p
The next day, take a new snapshot and send the difference from the last one both machines have:
btrfs subvolume snapshot -r /srv/www /srv/snapshots/www.2026-10-05btrfs send -p /srv/snapshots/www.2026-10-04 /srv/snapshots/www.2026-10-05 | ssh [email protected] btrfs receive /backup/web-01-p names the parent. The stream carries only what changed since it, and the backup host builds the new snapshot from its own copy of the parent, found by that received UUID. So every incremental needs the parent on both machines, read-only and unchanged:
- Keep the newest snapshot on the server until the next send has succeeded; it is the next parent.
- Keep the matching copy on the backup host too. A copy made writable can no longer be a parent.
- If the chain breaks, send a full stream again.
Prune old snapshots
Snapshots keep every block they reference, so delete old ones. On the server, as root, keep the newest 14:
printf '%s\n' /srv/snapshots/www.* | head -n -14 | xargs -r btrfs subvolume deleteISO dates sort in time order, so head -n -14 prints all but the 14 newest, and xargs -r runs nothing on an empty list. Do the same on the backup host with a larger number, never small enough to delete the newest copy. The space returns in the background; btrfs subvolume sync /srv waits for it.
Automate it with btrbk or snapper
Two open-source tools do this bookkeeping. btrbk (0.32.5 in Ubuntu 24.04) takes snapshots, sends them incrementally to local or SSH targets, picks parents itself and prunes both sides by a retention policy. For this layout:
timestamp_format long
snapshot_preserve_min 2d
snapshot_preserve 14d
target_preserve_min no
target_preserve 20d 10w 6m
volume /srv
snapshot_dir snapshots
subvolume www
target ssh://backup.example.com/backup/web-01Snapshots are named like www.20261004T0217. The server keeps every snapshot for two days and a daily one for 14; the backup host keeps 20 daily, 10 weekly and 6 monthly copies. Both _preserve_min settings default to all, which deletes nothing. Preview a run, then schedule btrbk -q run daily, for example from cron:
sudo btrbk run -nbtrbk has no restore command; its documentation restores with the plain btrfs steps below. snapper (0.10.6 in Ubuntu 24.04) manages snapshots on the same filesystem: after snapper -c www create-config /srv/www, and with TIMELINE_CREATE on, it takes hourly snapshots into /srv/www/.snapshots, prunes them by the limits in /etc/snapper/configs/www, and can show (snapper -c www diff 3..4) or undo (snapper -c www undochange 3..4) changes between two. Snapper 0.12 and later add snbk, which transfers its snapshots to local and remote Btrfs filesystems; Ubuntu 24.04's package predates it.
Restore files or a whole subvolume
On the backup host every copy is a read-only directory tree. Copy a few files back with rsync:
rsync -a /backup/web-01/www.2026-10-04/html/wp-config.php [email protected]:/srv/www/html/For the whole subvolume, send any copy back in full (each is complete), unless the server still has that snapshot:
btrfs send /backup/web-01/www.2026-10-04 | ssh [email protected] btrfs receive /srv/snapshotsThen on the server, as root, with the services that write to /srv/www stopped:
mv /srv/www /srv/www.brokenbtrfs subvolume snapshot /srv/snapshots/www.2026-10-04 /srv/wwwWithout -r the new /srv/www is writable. Start the services, check the site, then btrfs subvolume delete /srv/www.broken. Keep www.2026-10-04 on both machines as the next parent. On a new server, create the Btrfs filesystem and /srv/snapshots first, then receive and snapshot the same way.
Don't make a received snapshot writable with btrfs property set. It refuses (ERROR: cannot flip ro->rw with received_uuid set, use force if you really want that), and forcing it clears the received UUID that incremental sends depend on. Take a writable snapshot of it, as above.
Send to a file or object storage
With no Btrfs machine to receive on, store the stream as a file, for example in an S3 bucket:
btrfs send /srv/snapshots/www.2026-10-04 | zstd | aws s3 cp - s3://acme-server-backups/web-01/www.2026-10-04.btrfs.zstbtrfs send -p /srv/snapshots/www.2026-10-04 /srv/snapshots/www.2026-10-05 | zstd | aws s3 cp - s3://acme-server-backups/web-01/www.2026-10-05.btrfs.zstEach command in a stream carries a CRC32C checksum, and btrfs receive --dump reads a stream and validates it command by command without writing anything:
aws s3 cp s3://acme-server-backups/web-01/www.2026-10-04.btrfs.zst - | zstd -d | btrfs receive --dump > /dev/null && echo OKTo restore, receive the full file first, then each increment in order:
aws s3 cp s3://acme-server-backups/web-01/www.2026-10-04.btrfs.zst - | zstd -d | btrfs receive /srv/snapshots- A restore needs a Btrfs filesystem with room for the whole subvolume, and
btrfs receive. You can't list a stream like a tar archive or take out one file without receiving it all. - Increments form a chain: lose one and every later one is useless. Start a new full stream regularly, such as monthly.
- In a script, use
set -o pipefail. Without it the pipeline returns the upload's exit status, so a send that fails halfway looks like a success while the object holds a truncated stream (why backups fail silently). - Above 50 GB,
aws s3 cpneeds--expected-sizein bytes for a stream, or the upload can run out of parts. - btrbk's raw targets (experimental, per its README) write streams as files, optionally compressed and GnuPG-encrypted.
See how much space snapshots use
A new snapshot takes almost no space and grows as the live subvolume changes, because it keeps the old blocks. df can't say which snapshot holds what; btrfs filesystem du can:
sudo btrfs filesystem du -s /srv/www /srv/snapshots/*Total is the data the files reference, Exclusive the part nothing else shares, and Set shared the shared part, each extent counted once. It reads every file's extent map, so it is slow on large trees. Quota groups keep running totals instead:
sudo btrfs quota enable /srvsudo btrfs qgroup show /srvEach subvolume and snapshot has a qgroup 0/ plus its ID from btrfs subvolume list; its Exclusive figure is what deleting it would free. Quotas add work to every write, and the btrfs manual advises enabling them only if you use them; sudo btrfs quota disable /srv turns them off.
Common errors
| Message | Cause | Fix |
|---|---|---|
ERROR: subvolume /srv/www is not read-only | You sent the live subvolume or a writable snapshot | Send a snapshot taken with -r. |
ERROR: parent subvolume /srv/snapshots/www.2026-10-04 is not read-only | The -p parent is writable | Use read-only snapshots as parents. |
ERROR: not dumping send stream into a terminal, redirect it into a file | btrfs send with nothing to send the stream to | Pipe it into btrfs receive, or use -f to write a file. |
ERROR: /backup/web-01 doesn't belong to btrfs mount point | The receiving directory is not on Btrfs (we got this on ext4) | Receive onto a Btrfs filesystem, or store the stream as a file. |
ERROR: snapshot: cannot find parent subvolume ... followed by a UUID | The -p parent is missing on the receiving side, or its copy was changed | Send from a snapshot both sides still have, or send a full stream. |
ERROR: creating subvolume www.2026-10-04 failed: File exists | The target has that name already, often a partial copy from an interrupted transfer | A complete copy shows Flags: readonly and a Received UUID; delete a partial one and send again. |
ERROR: empty stream is not considered valid, or with --dump, ERROR: failed to dump the send stream: No data available | No data arrived, usually because send failed first or the file is empty | Read the error from btrfs send above it. |
ERROR: cannot flip ro->rw with received_uuid set, use force if you really want that | btrfs property set on a received snapshot | Take a writable snapshot of it instead. |
Frequently asked questions
- Is a Btrfs snapshot a backup?
- Not on its own. It shares blocks with the live data on the same filesystem, so a disk failure or a deleted server takes both. Copy it to another machine with
btrfs sendandbtrfs receive. - How much space does a Btrfs snapshot use?
- Almost none when taken. It grows as the original changes, because it keeps the old versions of changed and deleted blocks.
btrfs filesystem duand quota groups show how much. - Can I restore a single file from a Btrfs snapshot?
- Yes. A snapshot is a read-only directory, so copy the file out with
cp -a. From a stream stored as a file, you must receive the stream into a Btrfs filesystem first. - What is the difference between btrfs send -p and -c?
-pnames the parent the incremental stream is made against.-cadds clone sources that the stream may share data with; they must be identical on both sides, and with only-cbtrfs picks a parent from among them.
How this was checked
Commands, limits and prices were checked against these official pages, on October 4, 2026:
- btrfs-subvolume(8), btrfs-progs documentation
- btrfs-send(8), btrfs-progs documentation
- btrfs-receive(8), btrfs-progs documentation
- btrfs-progs documentation: Send/receive
- btrfs-filesystem(8): du
- btrfs-qgroup(8)
- btrfs-quota(8)
- btrfs-property(8)
- btrfs-subvolume(8), btrfs-progs 6.6.3 (Ubuntu 24.04)
- btrfs-progs 6.6.3 source: send error messages
- btrfs-progs 6.6.3 source: receive error messages
- btrbk README (configuration, raw targets, restoring)
- btrbk.conf(5)
- btrbk(1)
- snapper(8) (Ubuntu 24.04)
- snapper-configs(5) (Ubuntu 24.04)
- snbk(8) source, snapper repository
- snapper changelog (snbk added in 0.12.0)
- Ubuntu packages: snapper in Ubuntu 24.04
- Ubuntu packages: btrbk in Ubuntu 24.04
- AWS CLI reference: s3 cp (streams and --expected-size)
- zstd(1) (Ubuntu 24.04)