ZFS: cannot destroy snapshot, it has holds
Your zfs destroy fails with dataset is busy or dependent clones. Find the hold, release the tag, use deferred destroy, and promote a clone safely.
Why you cannot destroy a ZFS snapshot
You cannot destroy a ZFS snapshot for one of two unrelated reasons, and zfs destroy does not clearly say which one applies. Either something placed a hold on the snapshot, which is a named lock that survives reboots, or a clone was made from the snapshot and still reads its blocks. Work out which one you have before typing anything else. The fix for a hold does nothing for a clone, and the fix for a clone destroys data if you guess wrong.
This page assumes you already have a pool and a snapshot that will not go away. Every command here is one you run on your own pool. Read the output each time and answer the question in the text before moving to the next step.
Read the exact error message first
The same command prints two different messages. The first one:
cannot destroy snapshot tank/data@daily-2026-09-01: dataset is busydataset is busy means the snapshot itself is locked in place. A user hold is the usual cause. Two other causes exist and are covered near the end of this guide.
The second one:
cannot destroy 'tank/data@daily-2026-09-01': snapshot has dependent clones
use '-R' to destroy the following datasets:
tank/stagingThat is the clone case. The snapshot is the origin of the dataset listed under the message. ZFS prints the name of every dependent for you, so read the list. tank/staging may be something you use every day. Ignore the -R suggestion for now. It is the most destructive option on the page and it is offered first.
Who is holding the snapshot?
zfs holds tank/data@daily-2026-09-01
zfs holds -r tank/data@daily-2026-09-01zfs holds prints one line per hold, with the snapshot name, the tag, and the time the hold was taken. The -r form adds the holds on the descendant snapshots of the same name, which matters when a recursive destroy fails because of one held snapshot deep in the tree while every other snapshot was ready to go.
The hold count is also a property, which is easier to scan across a whole pool:
zfs get userrefs tank/data@daily-2026-09-01
zfs list -t snapshot -r -o name,userrefs,defer_destroy tankAsk two questions of that output. Is userrefs above zero on the snapshot you want gone? Does the tag look like a tool you run? Tags are free text, and software that takes holds names its tags after itself, so a tag starting with zrepl_ belongs to zrepl. The tag is often the only evidence you get about who is responsible.
Where holds come from in the first place
A hold exists to stop a snapshot disappearing while something still needs it. Replication is the main user. An incremental zfs send needs the previous snapshot to still exist on both ends, so a replication tool takes a hold on it and releases that hold once the next increment has landed. Replicating snapshots to a storage VPS with zfs send is exactly the workflow that leaves holds behind.
The important mechanism is this: a hold has no expiry. ZFS never times one out, and a reboot does not clear one, because the hold is stored in the pool and not in memory. So any job that dies between taking the hold and releasing it leaves that hold in place forever, and nothing will ever notice. The common sources:
- a replication run killed by an SSH timeout, or by the machine rebooting mid stream
- a backup tool that crashed before it reached its release step
- a tool you removed from the box while its holds were still live
- a hold someone took by hand during an incident and never released
An interrupted receive leaves a different mark, and it sits on the destination side. Check for it:
zfs get receive_resume_token tank/dataA long token string means a resumable receive was interrupted and its partial state is still on disk. You have two choices. Resume it, by handing the token back to the sender with zfs send -t <token>. Or abandon it and free the partial state:
zfs receive -A tank/dataFinally, check whether a send is running right now with pgrep -a zfs. A snapshot that is being sent is busy for as long as the send process lives, and no hold appears in zfs holds for it.
Release the tag, not the snapshot
A hold is removed by name. You release the tag, and the snapshot becomes destroyable again once the last tag is gone.
zfs release backup-nightly tank/data@daily-2026-09-01
zfs holds tank/data@daily-2026-09-01The tag is matched exactly. Get it wrong and ZFS answers no such tag on this dataset, which means you mistyped it rather than that the hold vanished. Copy the tag from the zfs holds output instead of retyping it. Add -r to release the same tag across descendant snapshots.
When zfs holds prints nothing, run the destroy again.
Before you release anything, work out what the tag belongs to. Releasing a hold that a live replication job still needs is how you break a backup chain. If that snapshot was the most recent one both sides share, the next incremental receive fails:
cannot receive incremental stream: most recent snapshot of tank/data does not match incremental sourceThe recovery from that is a full send of the whole dataset. On a slow link to another city, that is days of transfer to undo one release that took a second. Stop the replication tool, or check its state, before you touch its holds.
What a deferred destroy does, and when to use it
If you cannot tell whether the holder still needs the snapshot, do not release the hold. Mark the snapshot instead:
zfs destroy -d tank/data@daily-2026-09-01
zfs get defer_destroy tank/data@daily-2026-09-01-d means: destroy it now if that is possible, and if it is not, remember that I asked. The property reads on afterwards. The snapshot then stays in place until the last hold is released, and at that moment it is destroyed automatically with nobody watching. The same applies to the clone case: a deferred snapshot goes when the last clone on it is destroyed or promoted away. On a snapshot with no holds and no clones, -d simply destroys it immediately.
Deferred destroy is the right answer when the holder is a job you expect to finish, or a tool you are decommissioning, and you want the space back the moment it lets go. It is the wrong answer when you need space today, because -d frees nothing today. Check what you actually have with zpool list and zfs list -o space tank before deciding.
Treat -d as a decision rather than a pause. The snapshot will disappear at a time you do not control. Never mark a snapshot you might still want to roll back to.
Why the clone case needs zfs promote
A clone is a writable dataset that starts from a snapshot and shares every block with it. Nothing is copied when the clone is created, so the origin snapshot's blocks are the clone's data. Destroying that snapshot would remove blocks the clone is still reading, so ZFS refuses. How ZFS snapshots, clones and rollback share blocks covers that relationship in full.
Find the dependents from either side:
zfs get clones tank/data@daily-2026-09-01
zfs list -r -o name,origin tankThe clones property lists the datasets made from that snapshot. The origin column shows the same link from the clone's side, which is the view you want when auditing a pool you inherited.
Now you have two honest options. If the clone is disposable, destroy the clone by name, then destroy the snapshot. If the clone is the dataset you actually care about, promote it:
zfs promote tank/stagingzfs promote reverses the parent and child relationship. The origin snapshot, and every snapshot older than it, moves from tank/data to tank/staging. The clone becomes an independent filesystem, and the original dataset becomes the clone of it. The snapshot names move too: tank/data@daily-2026-09-01 is now tank/staging@daily-2026-09-01.
This is why the snapshot still refuses to be destroyed straight after a promote. The dependency did not go away. It turned around. Promotion is the step that lets you destroy the old dataset, which is almost always the real goal: you cloned something, the clone became the live copy, and now you want the original gone. Destroy tank/data, and the snapshot it depended on goes with it.
Two things to watch after a promote. Space accounting moves with the snapshots, so tank/staging reports a much larger used value straight afterwards, and any quota or refquota on it is now measured against that larger number. And a promote fails when both datasets hold snapshots with the same name, because the moved snapshot would collide with an existing one. ZFS reports the conflicting snapshot and changes nothing, so rename one of them and run it again.
Why -R is not a shortcut
The error message itself suggests -R, and that suggestion has cost people their data. -R destroys the snapshot and everything that depends on it: dependent clones anywhere in the pool, the children of those clones, and all of their snapshots. There is no confirmation prompt and no undo. A clone does not look different from an ordinary filesystem in zfs list, so -R aimed at a snapshot with an old, forgotten clone can take a live filesystem with it.
Run the dry run every time:
zfs destroy -nv tank/data@daily-2026-09-01
zfs destroy -nv -R tank/data@daily-2026-09-01-n does nothing and -v prints what would have happened, so you get the full list of datasets that would be destroyed and the space that would be reclaimed. Read every line. If a name surprises you, stop and find out what it is before going further.
-r and -R are different letters with different reach. -r destroys the snapshot of that name on the dataset and on all its descendants. -R adds every dependent clone, including clones that live outside the part of the tree you named. Neither one belongs in a script that runs unattended. And a snapshot is not a backup, so a mistake made here is not recoverable from the same pool.
An order that works
- Run
zfs destroy -nvon the snapshot and read the message. - If the message names dependent clones, go to the clone steps: list them with
zfs get clones, then destroy or promote. - If the message says
dataset is busy, runzfs holds -ron the snapshot. - If
userrefsis zero, look for a mounted snapshot directory and for a running send. - Identify what owns each tag before releasing it, and stop that tool first if it is still running.
- Release the tag, or mark the snapshot with
zfs destroy -dif you cannot identify the owner. - Run the destroy again, without
-R.
Busy, but no holds and no clones
If zfs holds prints nothing, userrefs is 0, clones is empty, and the destroy still says dataset is busy, two causes are left on Linux.
The snapshot is mounted. Browsing /tank/data/.zfs/snapshot/daily-2026-09-01 mounts that snapshot read only, and a mounted snapshot cannot be destroyed. Check the mount table and unmount it:
grep snapshot /proc/mounts
sudo umount /tank/data/.zfs/snapshot/daily-2026-09-01If the unmount also reports that the target is busy, a process still has a file open under that path. Find it with sudo lsof +D /tank/data/.zfs/snapshot/daily-2026-09-01 and deal with that process. OpenZFS also unmounts an idle automounted snapshot by itself after a few minutes, so a destroy that fails immediately after someone browsed the snapshot directory often succeeds if you wait and retry.
The other cause is a send or receive in flight, which pgrep -a zfs will show. Let it finish, or stop it deliberately, rather than killing it and leaving resume state behind.
Stop collecting holds
Audit after every failed replication run, while you still remember which job it was. zfs list -t snapshot -r -o name,userrefs,defer_destroy tank gives you the whole pool in one screen, and any non-zero userrefs on an old snapshot is a job that never finished cleaning up.
Use a bookmark where you only need a send position, not the data:
zfs bookmark tank/data@daily-2026-09-01 tank/data#daily-2026-09-01A bookmark records where a snapshot was in the stream without keeping its blocks, so you can destroy the snapshot, free the space, and still send an incremental using the bookmark as the source. A bookmark cannot be read, mounted or rolled back to. It is only a send source, and that is the point: it costs almost nothing to keep.
Holds belong to the same family as the other ZFS settings that are quiet until they are expensive, such as the pool properties you cannot change after creation. Write down which tool takes which tag, and keep the routine pool work on a schedule, including a scrub schedule sized for a small pool, so that the first time you look at a snapshot list is not during an incident.
FAQ
How do I find out what is holding a ZFS snapshot?
Run zfs holds tank/data@snapname, adding -r to include descendant snapshots of the same name. Each line gives the snapshot, the tag, and the time the hold was taken. The tag is free text chosen by whoever took the hold, and replication tools name their tags after themselves, so the tag usually identifies the owner. For a pool-wide view use zfs list -t snapshot -r -o name,userrefs,defer_destroy tank, where userrefs is the number of holds on each snapshot.
Does zfs destroy -d free the space right away?
No. -d marks the snapshot for deferred destruction and frees nothing at the time you run it. The snapshot is destroyed automatically once the last hold is released, or once the last clone on it is destroyed or promoted away. Confirm the mark with zfs get defer_destroy tank/data@snapname, which reads on. If you need space today, you must find the holder and release the hold, or destroy the clone.
Why does destroying a snapshot need zfs promote?
Because a clone made from that snapshot shares its blocks, so removing the snapshot would remove data the clone is still reading. zfs promote tank/clone reverses the relationship: the snapshot and everything older moves to the clone, the clone becomes an independent filesystem, and the original dataset becomes the clone. The snapshot still cannot be destroyed on its own after that, because the dependency turned around rather than disappearing. What promotion gives you is the ability to destroy the old dataset, which takes the snapshot with it.
Is zfs destroy -R safe if I only want one snapshot gone?
No. -R destroys the snapshot plus every dependent clone anywhere in the pool, their children and their snapshots, with no prompt and no undo. Clones look like ordinary filesystems in zfs list, so an old clone you forgot about is destroyed silently. Run zfs destroy -nv -R tank/data@snapname first, read every name in the printed list, and only proceed if you recognise all of them.
zfs holds prints nothing, so why is the snapshot still busy?
Two causes remain. The snapshot may be mounted, because someone browsed /tank/data/.zfs/snapshot/snapname, and a mounted snapshot cannot be destroyed. Check grep snapshot /proc/mounts and unmount it, or wait a few minutes for the automount to expire. Or a zfs send or zfs receive is running against it, which pgrep -a zfs will show. On the receiving side, check zfs get receive_resume_token tank/data for interrupted state and clear it with zfs receive -A tank/data.