How to Configure Btrfs Send and Receive for Incremental Off-Site Backups

Eva Wong is the Technical Writer and resident tinkerer at ZimaSpace. A lifelong geek with a passion for homelabs and open-source software, she specializes in translating complex technical concepts into accessible, hands-on guides. Eva believes that self-hosting should be fun, not intimidating. Through her tutorials, she empowers the community to demystify hardware setups, from building their first NAS to mastering Docker containers.

Btrfs send and receive can turn read-only subvolume snapshots into an efficient off-site replication chain. The first transfer sends a complete snapshot. Later transfers use a previously replicated snapshot as the parent, so only the changes needed to reconstruct the new snapshot cross the network.

On ZimaOS, first confirm that both the source and off-site destination are actually Btrfs filesystems and that SSH access is available. ZimaSpace's current disk-format guide lists BTRFS read/write support, and the ZimaOS SSH guide shows how to enable terminal access from Developer Mode.

Understand the Backup Chain Before You Start

Btrfs send/receive is subvolume replication, not a generic directory-copy command. The source must be a Btrfs subvolume, and every snapshot used by btrfs send must be read-only. A read-only mount is not a substitute for a read-only subvolume snapshot.

The official btrfs send documentation describes two modes. A full send contains the complete snapshot. An incremental send uses -p or -c with snapshots that are available in the same state on both sender and receiver.

For a simple off-site chain, use one explicit parent with -p:

snapshot-A  --full send-->  off-site snapshot-A
snapshot-B  --send -p A-->  off-site snapshot-B
snapshot-C  --send -p B-->  off-site snapshot-C

Do not delete or modify the current parent until the next incremental transfer has completed and been verified.

Verify the Source Is a Btrfs Subvolume

Replace the example paths with the real mount points on your system. The snapshot directory should be outside the live source subvolume so that backup snapshots do not become nested inside the data being protected.

findmnt -no FSTYPE --target /mnt/pool/data
sudo btrfs subvolume show /mnt/pool/data
sudo btrfs version

The first command should report btrfs. The second must successfully identify /mnt/pool/data as a subvolume. If it is only a normal directory, stop here: btrfs send cannot send an arbitrary directory.

Run the same filesystem check on the off-site system for the receive location. btrfs receive must create its replicated subvolume on a Btrfs filesystem.

Prepare SSH Before Streaming Backup Data

An off-site send stream is binary filesystem data. SSH is a practical transport because it provides authentication and encryption in transit. If you are using ZimaOS on either endpoint, enable SSH first and test a normal login before attempting a Btrfs stream.

ssh backup@backup.example.net

For unattended jobs, use key-based SSH authentication. The remote account must also be able to run btrfs receive non-interactively. Do not let a remote sudo password prompt read from the same standard input that carries the Btrfs stream. A narrowly scoped privilege rule for the required receive operation is safer than broad passwordless root access.

Create the destination directory during an interactive administrative session:

ssh -t backup@backup.example.net \
  'sudo mkdir -p /mnt/backup/btrfs-recv'

Create the First Read-Only Snapshot

Create a fixed read-only snapshot of the live source subvolume. The -r flag matters because Btrfs incremental send depends on snapshots that cannot change underneath the send operation.

sudo mkdir -p /mnt/pool/.snapshots

sudo btrfs subvolume snapshot -r \
  /mnt/pool/data \
  /mnt/pool/.snapshots/data-20260831-1000

Confirm the property before sending:

sudo btrfs property get \
  /mnt/pool/.snapshots/data-20260831-1000 ro

The expected result is ro=true.

Send the Initial Full Snapshot Off-Site

The first transfer has no parent, so it is a full send. In a Bash-compatible shell, enabling pipefail makes a failure on either side of the pipeline visible to the calling shell.

set -o pipefail

sudo btrfs send \
  /mnt/pool/.snapshots/data-20260831-1000 \
  | ssh backup@backup.example.net \
  'sudo -n btrfs receive /mnt/backup/btrfs-recv'

If sudo -n fails on the remote host, fix the remote privilege setup before retrying. Do not replace it with a password prompt inside the streaming pipeline.

The official btrfs receive documentation notes that a successfully received subvolume becomes read-only. It also warns that users should not modify the receive path while a stream is being applied.

Verify the Received Snapshot Before Using It as a Parent

Do not assume that an SSH session ending means the backup chain is healthy. Inspect both snapshots:

sudo btrfs subvolume show \
  /mnt/pool/.snapshots/data-20260831-1000

ssh backup@backup.example.net \
  'sudo -n btrfs subvolume show \
  /mnt/backup/btrfs-recv/data-20260831-1000'

On the sender, note the snapshot UUID. On the receiver, the replicated subvolume should show that source identifier as its Received UUID. Also confirm that the received subvolume is read-only.

Only after this check should data-20260831-1000 become the parent for the next incremental backup.

Create and Send the Next Incremental Snapshot

After the live data changes, create a new read-only snapshot:

sudo btrfs subvolume snapshot -r \
  /mnt/pool/data \
  /mnt/pool/.snapshots/data-20260901-0200

Then send only the delta from the previous snapshot:

set -o pipefail

sudo btrfs send \
  -p /mnt/pool/.snapshots/data-20260831-1000 \
  /mnt/pool/.snapshots/data-20260901-0200 \
  | ssh backup@backup.example.net \
  'sudo -n btrfs receive /mnt/backup/btrfs-recv'

This works because the parent snapshot from the first send still exists in matching form on both systems. After the new snapshot is successfully received and verified, data-20260901-0200 can become the parent for the following run.

Keep the Parent Snapshots Identical

The most common way to break an incremental chain is to change the read-only state or contents of a snapshot that is being used as a parent. Btrfs tracks received snapshots with a received UUID specifically so the sender and receiver can identify corresponding history.

The official Btrfs guidance on subvolume flags and received UUIDs warns that changing a received snapshot from read-only to read-write breaks assumptions used by incremental send.

For that reason, do not make the off-site receive snapshot writable just to browse, restore, or edit files. If you need a writable recovery copy, create a separate snapshot from the protected receive snapshot:

sudo btrfs subvolume snapshot \
  /mnt/backup/btrfs-recv/data-20260901-0200 \
  /mnt/restore/data-20260901-0200

The new restore snapshot is writable by default, while the original receive snapshot stays intact for future incremental sends.

Use a Safe Snapshot Retention Rule

You do not need to keep every old snapshot forever, but you must keep the parent required by the next send on both systems. A simple rotation rule is:

  1. Create the new read-only source snapshot.
  2. Send it using the previous successful snapshot as -p.
  3. Verify the new off-site receive snapshot.
  4. Promote the new snapshot to be the next parent.
  5. Only then remove older recovery points according to your retention policy.

Keeping several historical snapshots can provide useful rollback points, but remember that a snapshot on the same filesystem is not an independent backup. The off-site replica is valuable because it puts another copy on a separate system and location. ZimaSpace's 3-2-1 backup guide explains why an off-site copy protects against failures that local redundancy cannot cover.

Handle Nested Btrfs Subvolumes Separately

Btrfs snapshots are not recursive across nested subvolumes. If /mnt/pool/data contains another subvolume, the parent snapshot contains a subvolume stub rather than a full snapshot of the nested data.

List subvolumes before finalizing the backup plan:

sudo btrfs subvolume list /mnt/pool

If important application data lives in nested subvolumes, create and replicate a separate read-only snapshot chain for each one.

Know When to Use -p and When to Use -c

For a linear backup history, -p is the simplest and easiest option to audit. The -c option can add one or more clone sources that let Btrfs reuse matching extents from additional snapshots, but those clone sources must also exist in exactly the same state on both ends.

If you cannot prove that a clone source is unchanged and present on both systems, do not use it. A straightforward one-parent chain is usually safer for an off-site backup job.

Optional: Use Protocol 2 for Compressed Extents

On sufficiently recent Linux and btrfs-progs versions, Btrfs send protocol 2 can transmit compressed extents more efficiently with --compressed-data. The official send documentation states that protocol 2 requires btrfs-progs 6.0 or later on both sender and receiver and Linux 6.0 or later on the sender.

sudo btrfs send \
  --proto 2 \
  --compressed-data \
  -p /mnt/pool/.snapshots/data-20260831-1000 \
  /mnt/pool/.snapshots/data-20260901-0200 \
  | ssh backup@backup.example.net \
  'sudo -n btrfs receive /mnt/backup/btrfs-recv'

Do not enable this only because the option exists. Check versions on both endpoints first, and use the default protocol when compatibility matters more than optimization.

Troubleshoot Common Incremental Send and Receive Failures

The send command says the snapshot is not read-only

Recreate the snapshot with btrfs subvolume snapshot -r. Merely mounting a writable snapshot through a read-only mount does not satisfy the send requirement.

The incremental send cannot find or use its parent

Check that the exact parent snapshot still exists on the sender and its corresponding received snapshot still exists on the receiver. If either parent was deleted, changed, or made writable, restore a matching parent if you have one. Otherwise create a new read-only snapshot and start a fresh full seed.

btrfs receive says the destination subvolume already exists

btrfs receive will not overwrite an existing subvolume with the same incoming name. Inspect the existing subvolume first. If it is a failed or incomplete receive and you have confirmed it is safe to remove, delete that incomplete subvolume before retrying the same transfer.

The receive parent was modified after it arrived

Do not use that modified snapshot as the basis of a new incremental stream. If there is no unchanged matching receive parent, start a new full backup chain.

Files inside a nested directory are missing from the snapshot

Check whether that directory is itself a Btrfs subvolume. Nested subvolumes are not recursively included in the parent snapshot and need their own send/receive chain.

The WAN or SSH connection drops during a transfer

Treat the receive as failed unless it completed successfully and the resulting subvolume verifies correctly. The documented Btrfs send/receive command interface does not provide a stream-resume option. For unreliable long-distance links, consider writing the send stream to a staging file, transferring that file with a resumable transport, and then feeding the completed trusted file to btrfs receive.

Protect the Receive Side from Untrusted Streams

Btrfs receive applies filesystem operations from the incoming stream. The official receive documentation advises against accepting send streams from untrusted sources and recommends protecting the receive path from concurrent writes while a stream is being applied.

Use SSH host verification, key-based authentication, a dedicated backup account, and the narrowest practical privileges. Keep the receive directory out of normal user write paths while a backup is running.

Use This Checklist for Each Incremental Run

  • Confirm both endpoints are Btrfs.
  • Create the new source snapshot with -r.
  • Keep the previous successful parent unchanged on both systems.
  • Send with btrfs send -p OLD NEW.
  • Receive over an authenticated and encrypted SSH connection.
  • Verify success and compare source UUID to receiver Received UUID.
  • Keep the received backup snapshot read-only.
  • Create a separate writable snapshot when you need to restore or test data.
  • Rotate old snapshots only after the new parent is verified.
  • Back up nested subvolumes with separate chains.

Once one full seed and one incremental run both succeed manually, automate the same sequence with logging and explicit exit-status checks. The important part is not the scheduler: it is preserving an unchanged, verified parent snapshot on both ends of every incremental step.

Support & Tips

More to Read

Get More Builds Like This

Stay in the Loop

Get updates from Zima - new products, exclusive deals, and real builds from the community.

Stay in the Loop preferences

We respect your inbox. Unsubscribe anytime.