A ZFS snapshot is a name attached to a dataset’s block tree at one transaction group. Nothing is copied, nothing is quiesced, and the cost is a few metadata blocks: zfs snapshot tank/data@before-upgrade returns before the shell prompt redraws. Because ZFS never overwrites a live block in place, the old tree stays valid on disk until the last reference to it goes away, and the snapshot is that reference.

That is what makes a nightly wall of history practical, and it is also where this article’s title gets dangerous. Snapshots give you many versions of the data; they do not give you safety from losing the pool. Every snapshot of tank/data lives in tank, so the failure that takes the pool takes the dataset and its history in the same instant. Multiversion and multiversion-safety are different properties, and only replication supplies the second.

A name for one consistent tree

Snapshots are created at a transaction group boundary, so the tree they capture is internally consistent — a database’s files and its metadata freeze at the same instant, not microseconds apart. -r snapshots a dataset and all its descendants, which is what you want for anything holding child datasets; without it, a recursive layout gets snapshots taken at different times at different levels, and incremental replication between them becomes awkward.

Names are the retention policy’s only handle: tank/data@daily-2026-09-16 is prunable by pattern, while an autogenerated name needs bookkeeping. zfs list -t snapshot -o name,used,refer,creation -s creation shows the set with the space each one pins.

Where the space is, in zfs list -o space

ZFS reports space in a column set that answers the only question that matters: whose data is it? zfs list -o space tank/data gives USED, USEDSNAP, USEDDS, USEDREFRESERV, USEDCHILD, REFER and AVAIL. REFER is what the dataset alone needs, USEDSNAP is what its snapshots pin, and USED is the sum. A rising USEDSNAP with a flat REFER means the snapshots, not the data, are eating the pool.

The fixture builds a wall of thirty daily snapshots over a 64 MiB dataset with 2 MiB of churn per day. Creation costs 0 KiB every single time — the cost lands on the write that follows, 2048 KiB for 2 MiB rewritten — and the whole wall costs 60 MiB against 1920 MiB for thirty full copies. That ratio is the entire argument for snapshots, and why nobody notices the pool filling until it is nearly full.

Holds, diff, and comparing versions

A hold is a named reference that makes destruction fail with EBUSY: zfs hold keep tank/data@daily-2026-09-16 protects a snapshot, zfs holds lists the holders, and zfs release keep clears it. Backup software holds the snapshot it is sending so retention cannot delete it; the cost is that pruning then fails until the hold is released, and a stuck hold can pin a snapshot for months.

zfs diff tank/data@daily-2026-09-15 tank/data@daily-2026-09-16 reports per-path changes — + created, - removed, M modified, R renamed — and both snapshots must still exist, which is why diffing against yesterday is easy and against last month may not be. Read a snapshot’s contents through the hidden .zfs/snapshot/<name> directory; there is no separate mount for a snapshot, and snapdir=visible makes it listable.

When it breaks

The failure that matters is the one the title invites: a pool loss. A failed controller, a backplane fault, or pool-wide corruption removes the dataset and every snapshot of it atomically; there is no partial survival. The only defence is a copy outside that pool: zfs send -i tank/data@yesterday tank/data@today | zfs receive backup/data writes an independent, checksummed stream into a second pool, and only that stream survives the first pool’s death. A snapshot wall with no replica is a single point of failure with a nicer interface.

The second failure is silent space creep plus retention that cannot run. USEDSNAP grows while REFER stays flat, the pool passes 80 percent, and ZFS starts refusing writes with no space left even though the datasets look reasonable — slop and reservations are not visible from inside the filesystem. If a snapshot is also held for a clone, destroying it fails, and rolling back past a clone’s origin forces a recursive destroy. Long-lived holds turn retention into a dependency graph, so keep hold lifetimes short and audited.

Check it quickly

See what the snapshots cost and whether they can be removed:

zfs list -t snapshot -o name,used,refer,creation -s creation tank/data
zfs list -o space tank/data
zfs holds tank/data@daily-2026-09-16
zfs diff tank/data@daily-2026-09-15 tank/data@daily-2026-09-16
zfs send -i tank/data@daily-2026-09-15 tank/data@daily-2026-09-16 | zfs receive backup/data

used per snapshot is the space that snapshot alone pins, and a fresh snapshot’s value is near zero; the space columns then show USEDSNAP against REFER, and a rising USEDSNAP with a flat REFER is the signal to prune. If zfs destroy fails with a busy snapshot, zfs holds names the tag to release. The last line is the only one that protects against losing the pool; run it on a schedule.

Sources

  • zfs-snapshot(8), snapshot creation semantics and recursive snapshots, checked 2026-09-16.
  • zfs-list(8), the space column set including USEDSNAP, USEDDS and REFER, checked 2026-09-16.
  • zfs-hold(8), holds, zfs holds, and the EBUSY returned by destroy, checked 2026-09-16.
  • zfs-send(8), incremental replication streams that leave the pool, checked 2026-09-16.