Send and Receive
zfs send serialises a snapshot into a byte stream, and zfs receive
turns that stream back into a dataset. Together they are ZFS’ native
replication and backup mechanism.
Because the stream is produced from ZFS’ own block-level bookkeeping, an
incremental send has to read only the blocks that actually changed between two
snapshots — it never walks the directory tree looking for modified files. That
makes replication cost proportional to the amount of changed data rather than
to the size of the dataset, which is the main reason to prefer it over
file-based tools such as rsync on ZFS.
Streams are checksummed, so corruption introduced by the network or by intermediate storage is detected on receive.
A full stream
Everything starts from a snapshot:
zfs snapshot pool/data@2026-07-21
zfs send pool/data@2026-07-21 | ssh backup zfs receive tank/data
The receiving side creates tank/data along with the snapshot
tank/data@2026-07-21. The target dataset must not already exist unless
you are receiving an incremental stream into it.
Streams are just bytes, so they can also be stored in a file or piped through anything:
zfs send pool/data@2026-07-21 | zstd -3 > /mnt/usb/data-full.zfs.zst
Storing streams in files is fine for transport, but it is a poor archive format: a single damaged byte can render the whole stream unreceivable, whereas a received dataset is checksummed, scrubable and repairable. Prefer receiving into a real pool for anything you intend to keep.
Incremental streams
-i sends the difference between two snapshots. The source snapshot must
still exist on the receiving side.
zfs snapshot pool/data@2026-07-22
zfs send -i pool/data@2026-07-21 pool/data@2026-07-22 \
| ssh backup zfs receive tank/data
-I sends every intermediate snapshot between the two, so the receiving
side ends up with the same snapshot history rather than just the endpoints:
zfs send -I pool/data@2026-07-21 pool/data@2026-07-28 | ...
The incremental source may also be a bookmark, which lets the sending side free the space held by the old snapshot while still being able to compute the difference:
zfs bookmark pool/data@2026-07-21 pool/data#2026-07-21
zfs destroy pool/data@2026-07-21
zfs send -i pool/data#2026-07-21 pool/data@2026-07-22 | ...
See Snapshots, Clones and Bookmarks for what bookmarks are.
Replicating a whole hierarchy
-R produces a replication stream package: the named dataset and all of its
descendants, up to the named snapshot. On receive, properties, snapshots,
descendant file systems and clones are preserved.
zfs snapshot -r pool/home@2026-07-21
zfs send -R pool/home@2026-07-21 | ssh backup zfs receive -u tank/home
Combined with -i/-I it becomes the usual “make the destination look
exactly like the source” tool. On the receiving side, -F then also destroys
snapshots and file systems that no longer exist on the sending side, and rolls
the target back to its most recent snapshot before receiving:
zfs send -R -I pool/home@sunday pool/home@monday \
| ssh backup zfs receive -F -u tank/home
-F discards data on the destination. Use -n -v first if you are not
sure what a stream will do.
-X excludes named datasets from a recursive send, and -s lets a
recursive send skip (with a warning) datasets whose snapshots are missing
instead of aborting.
Useful send options
-c(--compressed)Send blocks that are compressed on disk in their compressed form. The stream is smaller and both sides do less work, since nothing has to be decompressed and recompressed. This is the usual default choice for datasets with compression enabled.
-L(--large-block)Allow records larger than 128 KiB in the stream. Without it, a dataset with a larger
recordsizeis sent as smaller blocks, which is slower and changes the block layout on the receiving side. It has no effect if thelarge_blockspool feature is disabled orrecordsizewas never set above 128 KiB.-e(--embed)Send blocks that use embedded data (very small blocks stored inside the block pointer) in that form. Requires the
embedded_datafeature on both sides.-p(--props)Include dataset properties in the stream.
-Rimplies this.-w(--raw)Send the data exactly as it is on disk. For encrypted datasets this means the stream stays encrypted, so a backup can be taken without loading the keys, and the receiving machine never needs them. See Encryption.
-h(--holds)Include user holds on the sent snapshots.
-n -v/-PDry run with a size estimate;
-Pprints it in a machine-parsable form. Worth running before any large or destructive transfer.
Useful receive options
-uDo not mount the received file system. On a backup server this is almost always what you want — the destination should not shadow local paths.
-d/-eRewrite the target name:
-ddrops the first component of the source dataset name,-ekeeps only the last one. Both are relative to the named target file system.-o property=value/-x propertyOverride a property carried in the stream, or exclude it so the local setting (or inheritance) wins.
-o readonly=onon the destination prevents accidental writes that would force a rollback on the next incremental receive;-x mountpointis a common way to keep a backup copy from claiming the source’s mount point.-FRoll the destination back to its most recent snapshot first, and for incremental replication streams destroy datasets and snapshots that are gone on the sending side.
-sSave the partially received state if the transfer is interrupted, so it can be resumed instead of restarted. See below.
Resuming an interrupted transfer
A multi-terabyte initial replication over an unreliable link does not have to
start over. When the receiving side used -s, the partially received state
is kept and exposed as the receive_resume_token property of the target
dataset:
# receiving side
zfs receive -s tank/data
# ... connection dies ...
zfs get -H -o value receive_resume_token tank/data
# sending side, with that token
zfs send -t <token> | ssh backup zfs receive -s tank/data
This requires the extensible_dataset feature on the receiving pool. An
abandoned partial receive can be cleaned up with zfs receive -A, which
frees the space it holds.
Practical notes
Keep a common snapshot. Retention on both sides must leave at least one snapshot (or bookmark on the sender) in common, otherwise the next incremental fails and a full resend is needed.
Do not modify the destination. Any write breaks the next incremental
receive until the dataset is rolled back; readonly=on avoids it.
ssh is often the bottleneck. Both send and receive are bursty, so
buffering the pipe on both sides (mbuffer, dd with a large block size)
usually helps.
Feature options must match. Streams made with -L, -c, -e or
-w need the corresponding features on the receiving pool.
Doing the snapshot, retention, hold and resume-token bookkeeping by hand gets
tedious. Third-party tools built on zfs send/receive handle it — see
scheduling snapshots, since the same tools
usually do both.
See the FAQ for stream caveats such as
hole_birth.
Further reading
zfs-redact(8) — sending a stream with selected data removed
zstream(8) — inspecting and manipulating stream files