VPS Snaps

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.

10 min readUpdated Checked against official documentation

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:

Terminal
findmnt -n -o FSTYPE /srv
Terminal
sudo btrfs subvolume list /srv

The 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

Terminal
sudo mkdir -p /srv/snapshots
Terminal
sudo 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:

Terminal
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:

Terminal (backup host)
mkdir -p /backup/web-01

On the server, as root, send the first snapshot in full:

Terminal
btrfs send /srv/snapshots/www.2026-10-04 | ssh [email protected] btrfs receive /backup/web-01

The backup host creates /backup/web-01/www.2026-10-04 and makes it read-only when the stream ends. Check it there:

Terminal (backup host)
btrfs subvolume show /backup/web-01/www.2026-10-04

A 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:

Terminal
btrfs subvolume snapshot -r /srv/www /srv/snapshots/www.2026-10-05
Terminal
btrfs 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:

Terminal
printf '%s\n' /srv/snapshots/www.* | head -n -14 | xargs -r btrfs subvolume delete

ISO 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:

/etc/btrbk/btrbk.conf
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-01

Snapshots 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:

Terminal
sudo btrbk run -n

btrbk 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:

Terminal (backup host)
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:

Terminal (backup host)
btrfs send /backup/web-01/www.2026-10-04 | ssh [email protected] btrfs receive /srv/snapshots

Then on the server, as root, with the services that write to /srv/www stopped:

Terminal
mv /srv/www /srv/www.broken
Terminal
btrfs subvolume snapshot /srv/snapshots/www.2026-10-04 /srv/www

Without -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:

Terminal
btrfs send /srv/snapshots/www.2026-10-04 | zstd | aws s3 cp - s3://acme-server-backups/web-01/www.2026-10-04.btrfs.zst
Terminal
btrfs 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.zst

Each command in a stream carries a CRC32C checksum, and btrfs receive --dump reads a stream and validates it command by command without writing anything:

Terminal
aws s3 cp s3://acme-server-backups/web-01/www.2026-10-04.btrfs.zst - | zstd -d | btrfs receive --dump > /dev/null && echo OK

To restore, receive the full file first, then each increment in order:

Terminal
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 cp needs --expected-size in 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:

Terminal
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:

Terminal
sudo btrfs quota enable /srv
Terminal
sudo btrfs qgroup show /srv

Each 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

MessageCauseFix
ERROR: subvolume /srv/www is not read-onlyYou sent the live subvolume or a writable snapshotSend a snapshot taken with -r.
ERROR: parent subvolume /srv/snapshots/www.2026-10-04 is not read-onlyThe -p parent is writableUse read-only snapshots as parents.
ERROR: not dumping send stream into a terminal, redirect it into a filebtrfs send with nothing to send the stream toPipe it into btrfs receive, or use -f to write a file.
ERROR: /backup/web-01 doesn't belong to btrfs mount pointThe 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 UUIDThe -p parent is missing on the receiving side, or its copy was changedSend from a snapshot both sides still have, or send a full stream.
ERROR: creating subvolume www.2026-10-04 failed: File existsThe target has that name already, often a partial copy from an interrupted transferA 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 availableNo data arrived, usually because send failed first or the file is emptyRead the error from btrfs send above it.
ERROR: cannot flip ro->rw with received_uuid set, use force if you really want thatbtrfs property set on a received snapshotTake 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 send and btrfs 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 du and 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?
-p names the parent the incremental stream is made against. -c adds clone sources that the stream may share data with; they must be identical on both sides, and with only -c btrfs picks a parent from among them.

How this was checked

Commands, limits and prices were checked against these official pages, on October 4, 2026: