Send Btrfs Snapshots Safely, Full or Incremental
You will create a Btrfs send stream from an existing read-only snapshot, either as a complete stream or as changes since an earlier snapshot. The stream can be saved for transfer or piped to btrfs receive on another Btrfs filesystem. Allow 10 to 30 minutes for a first test, plus the time needed to transfer the snapshot data.
The route
Jump straight to the step you need, or tick off Done means at the end.
- 1. Check the installed command and choose the snapshots
- 2. Make a full stream for the first transfer
- 3. Receive the full stream on the destination
- 4. Send only changes from a parent snapshot
- 5. Use clone sources only when both sides match exactly
- 6. Choose protocol and diagnostic options deliberately
- 7. Diagnose failures without destroying your source
1. Check the installed command and choose the snapshots
This guide uses the installed btrfs-progs 6.6.3 command. It assumes that the source filesystem is mounted and that you already have snapshots at paths such as /btrfs/snapshots/home-2026-09-22. The snapshots must be read-only. A read-only mount is not enough: Btrfs send needs the snapshot itself to remain read-only, and the same subvolume must not be writable through another mount while the send is running.
$ btrfs --version
btrfs-progs v6.6.3
$ btrfs subvolume show /btrfs/snapshots/home-2026-09-22
The second command is an inspection step. Its output is host-specific, but it should identify a Btrfs subvolume. If it fails, stop and correct the path before attempting a send. Reading an existing snapshot normally does not require sudo; use elevated privileges only when the account cannot traverse the mount or read the snapshot.
2. Make a full stream for the first transfer
A full send contains the entire snapshot data and metadata. Give the output a new name so that an existing backup cannot be silently replaced. The -f option writes the stream to a file instead of standard output.
$ snapshot=/btrfs/snapshots/home-2026-09-22
$ stream=/var/backups/btrfs/home-2026-09-22.full.stream
$ test ! -e "$stream" || { printf 'refusing to overwrite %s\n' "$stream" >&2; exit 1; }
$ btrfs send -f "$stream" "$snapshot"
$ printf 'send exit status: %s\n' "$?"
send exit status: 0
$ test -s "$stream" && wc -c < "$stream"
1843200
The byte count is illustrative: your stream will differ with the snapshot contents. A successful command returns status 0 and creates a non-empty stream. The stream is binary, so do not inspect it in a text editor. Keep the source snapshot unchanged until the stream has been received and checked.
Checkpoint: at this point you have a complete stream that can be copied to the receiving host. Do not delete or alter the source snapshot merely because the file exists.
3. Receive the full stream on the destination
Copy the stream to a directory on the destination Btrfs filesystem, then pass it to btrfs receive. The destination argument is a mounted directory, not a device path. Receiving creates the sent subvolume, so choose a directory where that name is expected and where you have enough free space.
destination=/srv/btrfs-receive
test -d "$destination" && mountpoint -q "$destination"
btrfs receive "$destination" < /var/backups/btrfs/home-2026-09-22.full.stream
printf 'receive exit status: %s\n' "$?"
receive exit status: 0
The mountpoint check is a guard against feeding a stream into an ordinary directory on the wrong filesystem. Receiving changes the destination filesystem and is not an operation to run casually in a production path. Confirm the destination and the stream name before pressing Enter. If receive fails, keep the stream and inspect the error before retrying. Do not delete a partially received subvolume until you have identified it with btrfs subvolume list "$destination" and confirmed that removal is safe.
4. Send only changes from a parent snapshot
After the first snapshot has been received, an incremental send can use that earlier snapshot as its parent. The parent and the new snapshot must be read-only, and the parent must be available on both the sending and receiving sides in the corresponding state.
$ parent=/btrfs/snapshots/home-2026-09-22
$ current=/btrfs/snapshots/home-2026-09-23
$ stream=/var/backups/btrfs/home-2026-09-23.incremental.stream
$ test ! -e "$stream" || { printf 'refusing to overwrite %s\n' "$stream" >&2; exit 1; }
$ btrfs send -p "$parent" -f "$stream" "$current"
$ printf 'send exit status: %s\n' "$?"
send exit status: 0
$ test -s "$stream" && wc -c < "$stream"
245760
This stream describes the changes from parent to current; it is not a self-contained replacement for the full stream. Receive it on the destination where the matching parent snapshot already exists. If the parent is missing, was changed, or is not the same snapshot state, the incremental workflow cannot reconstruct the intended result.
Checkpoint: record the parent used for every incremental stream. A simple naming convention such as home-2026-09-22 in both the snapshot and stream names makes recovery easier.
5. Use clone sources only when both sides match exactly
The -c option supplies an additional snapshot that Btrfs can use as a clone source. It can reduce the data represented by an incremental stream, and multiple -c options are allowed. It is also a sharp safety boundary: every clone source must be in exactly the same state on the sender and receiver.
$ btrfs send -p "$parent" -c /btrfs/snapshots/home-2026-09-15 \
-f "$stream" "$current"
Do not add -c because a similarly named snapshot happens to exist on the destination. A changed read-only status or changed contents can invalidate the assumption. If you cannot prove that the destination clone source is the same received snapshot, omit -c and use only the verified parent.
6. Choose protocol and diagnostic options deliberately
The default send protocol is version 1. Protocol 2 encodes file data more efficiently and is required for --compressed-data. Protocol 2 needs at least btrfs-progs 6.0 on both ends and Linux 6.0 on the sender. Passing --proto 0 asks for the highest version supported by the running kernel, so use it only when the receiver's compatibility is known.
$ btrfs send --proto 2 -f "$stream" "$current"
$ btrfs send --compressed-data -f "$stream" "$current"
$ btrfs send --no-data "$current" > metadata-only.stream
--compressed-data can preserve filesystem-compressed data in the stream, but it still requires protocol 2 or higher. --no-data deliberately omits file data and therefore cannot transfer the snapshot; it is useful for inspecting metadata differences, not for a restorable backup. Do not confuse a successful metadata-only command with a usable backup.
Use -v when you need readable generated-command diagnostics. Use -q when a script should suppress non-error messages. The stream itself still needs to be saved or piped; diagnostic output is not a substitute for it.
7. Diagnose failures without destroying your source
A non-zero status means the send failed. Check the snapshot paths, read permissions, available space for a file output, and whether every involved snapshot is read-only. For an incremental send, verify that the parent is the intended earlier snapshot. For clone sources, verify exact identity on both sides rather than retrying with more options.
$ test -r "$parent" && echo parent-readable
parent-readable
$ test -r "$current" && echo current-readable
current-readable
$ btrfs subvolume show "$parent" | sed -n '1,12p'
$ btrfs subvolume show "$current" | sed -n '1,12p'
Do not change snapshot flags while a send operation uses them. If an output file is incomplete, move it aside for investigation rather than presenting it as a backup. A failed receive does not make the stream trustworthy; retain the original stream and investigate the destination state before attempting cleanup.
Done means
- The installed version and exact source snapshot paths were checked.
- Every snapshot involved in the send is read-only and will remain unchanged during the operation.
- A full stream was created before any incremental stream was attempted.
- Incremental parents and clone sources are recorded and match the destination state.
- The stream is non-empty, receive returned status 0, and the received subvolume was checked.
- Protocol 2, compressed data and metadata-only mode were used only when their compatibility or limitations were understood.