You run zfs destroy on a dataset or snapshot and get one of two errors back:
cannot destroy 'tank/data': dataset is busy
or:
cannot destroy 'tank/data@daily-2026-07-20': snapshot has dependent clones
Both mean the same thing at a mechanical level: something still references the object you are trying to remove, and ZFS refuses to guess which reference you are willing to lose. The frustrating part is that the error tells you almost nothing about what holds the reference. The job here is to find the dependent, understand why it exists, and remove it in the right order.
One distinction up front: this article covers EBUSY-style failures where a dependent or a reference blocks destruction. A destroy that fails or hangs because the pool is completely full is a different failure mode (COW metadata updates themselves need free space) and belongs in capacity triage, not here.
What this means
ZFS datasets, snapshots, and clones form a dependency graph:
- A snapshot is a read-only point-in-time view of a dataset.
- A clone is a writable dataset created from a snapshot. The clone depends on its origin snapshot until you promote or destroy the clone. You cannot destroy a snapshot that has clones.
- A hold is a user-created tag pinned on a snapshot, typically placed by backup or replication tools so retention jobs cannot destroy a snapshot out from under an incremental send. Destroying a held snapshot returns EBUSY.
- A mounted or open dataset is busy in the kernel sense: a mountpoint, an open file handle, an NFS export, or a zvol mapped by a hypervisor all count as references.
The destroy path checks all of these and stops at the first blocker. Your job is to enumerate the blockers instead of blindly retrying with bigger flags.
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Clone depends on the snapshot | “snapshot has dependent clones” | zfs get clones <snapshot> and zfs get origin on suspects |
| Hold tag pins the snapshot | “dataset is busy” on a snapshot destroy | zfs holds -r <pool> or the userrefs property |
| Dataset still mounted or in use | “dataset is busy” on a filesystem destroy | zfs get mounted, then mount tables |
| Mount inside a container namespace | Host shows nothing mounted, destroy still fails | grep <dataset> /proc/*/mountinfo |
| Lazy unmount left a reference | Mountpoint gone from /proc/mounts, still busy | Recent umount -l in shell history or scripts |
| NFS export holds the dataset | No local mount, still busy | exportfs -v |
| VM process holds a zvol open | “dataset is busy” on a zvol destroy | fuser -am /dev/zvol/<pool>/<dataset> |
Quick checks
These are all read-only and safe to run on a production pool.
# Is the dataset itself mounted right now?
zfs get mounted,mountpoint tank/data
# Does this snapshot have dependent clones?
zfs get clones tank/data@daily-2026-07-20
# Which snapshot is this dataset cloned from?
zfs get origin tank/data-copy
# List all holds in the pool, recursively
zfs holds -r tank
# How many user holds does this snapshot have?
zfs get userrefs tank/data@daily-2026-07-20
# Search every process mount namespace for the dataset name
grep -l "tank/data" /proc/*/mountinfo 2>/dev/null
# Check for active NFS exports
exportfs -v
# Find processes holding a zvol open
fuser -am /dev/zvol/tank/vm-100-disk-0
# See space committed to snapshots and children per dataset
zfs list -o space -r tank
The grep /proc/*/mountinfo check is the single most valuable one in container environments. A mount that exists only inside a container’s mount namespace is invisible in the host’s /proc/mounts, and ZFS still sees the dataset as busy. This is the classic trap when deleting LXD/LXC containers backed by ZFS.
How to diagnose it
Work through the blockers in this order. The error message usually hints at the branch: “dependent clones” means clone path, plain “dataset is busy” means holds or mounts.
flowchart TD
A[zfs destroy fails] --> B{Error text}
B -->|has dependent clones| C[zfs get clones on snapshot]
B -->|dataset is busy| D[zfs holds -r]
C --> E[promote clone or destroy it]
D -->|holds found| F[zfs release tag]
D -->|no holds| G{Snapshot or filesystem?}
G -->|filesystem| H[check mounts, namespaces, NFS, zvol users]
G -->|snapshot| I[recheck holds with -r, check userrefs]
H --> J[umount, exportfs -u, or stop the process]Read the error carefully. “snapshot has dependent clones” is definitive: go to step 2. Bare “dataset is busy” on a snapshot is almost always a hold: go to step 3. On a filesystem or volume, it is a mount or open handle: go to step 4.
Map the clone dependency. Run
zfs get clones <snapshot>to see exactly which clones depend on the snapshot, andzfs get origin <clone>on each to confirm the relationship. Decide per clone: is it still in use, or is it leftover from an old test, template, or container?Enumerate holds. Run
zfs holds -r <pool>(recursive, so you see holds anywhere in the hierarchy) andzfs get userrefs <snapshot>. A non-zerouserrefswith no output fromzfs holdson that snapshot usually means the holds are on a different snapshot in the chain or you did not use-r.Hunt hidden mounts. If the object is a filesystem and
zfs get mountedsays no (or you already unmounted it), check mount namespaces withgrep -l "<dataset>" /proc/*/mountinfo. If a PID comes back, that process has the dataset mounted inside its own namespace. For zvols, checkfuser -am /dev/zvol/<pool>/<name>for hypervisor processes holding the device open.Check exports and lazy unmounts.
exportfs -vshows NFS exports, which hold a reference even with no client connected. If someone ranumount -learlier, the mountpoint looks gone but the reference lingers until the last user exits; the cleanest resolution is usually to finish whatever holds it or reboot.Resolve, then retry the destroy. Apply the matching fix below, then re-run the same
zfs destroycommand. Do not escalate to-Runtil you know exactly what the dependents are.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
usedbysnapshots per dataset | Clone and snapshot chains pin space you think you deleted | Snapshot space dominating a dataset’s usage |
userrefs on snapshots | Reveals hold accumulation from backup tooling | Holds with no owner you can identify |
Pool freeing property | Shows async reclaim in progress after destroys | Large freeing backlog that never drains |
| Snapshot count and age | Retention jobs that create but never prune lead to this error | Snapshot count growing monotonically week over week |
| Clone count per pool | Every clone is a future “dependent clones” error | Clones surviving past the task they were created for |
Fixes
Clone dependencies: promote or destroy
You have three real options, and they are not equivalent:
Destroy the clone. If the clone was temporary (a test environment, a container layer, a sandbox), destroy it first, then destroy the snapshot:
# Destroys data in the clone. Verify it is expendable first.
zfs destroy tank/data-copy
zfs destroy tank/data@daily-2026-07-20
Promote the clone. zfs promote <clone> reverses the dependency: the clone becomes the origin and the original dataset becomes a clone of it. After promotion you can destroy the original dataset, but understand what happened: promote does not break the dependency, it swaps parent and child. The blocks are still shared; you have not made the clone independent. If the clone diverged heavily and you want a fully independent copy, the only route is zfs send | zfs receive into a new dataset.
Force the recursive destroy. zfs destroy -R <snapshot> recursively destroys all dependents, including clones outside the target hierarchy. This is destructive across dataset boundaries and the list of casualties may be longer than you expect. Run it with the dry-run flag first (-n -v) to see exactly what would be destroyed. Never use -R as a first response to an error you have not diagnosed.
Holds: release the tag
List the tags, confirm which tool created them (the tag name usually tells you: backup software, replication tooling, sanoid-style retention), then release:
zfs holds tank/data@daily-2026-07-20
# Destructive in intent: removes the protection the hold provides
zfs release <tag> tank/data@daily-2026-07-20
Only release a hold when you know why it exists. Holds placed by replication tooling protect the common snapshot that the next incremental send depends on. Releasing and destroying it can force the next replication into a full send.
If you want the snapshot gone but it is still held, zfs destroy -d <snapshot> marks it for deferred destruction: it is automatically destroyed when the last hold is released and the last clone goes away. This is the clean answer when a retention script will release the hold on its own schedule.
Mounted or in-use datasets: unmount at the right layer
For an ordinary mount:
zfs umount tank/data
# or
umount /tank/data
For a mount trapped in a container namespace, find the PID from /proc/*/mountinfo and unmount inside that namespace:
# <PID> holds the dataset in its mount namespace
nsenter -t <PID> -m -- umount <mountpoint>
For NFS, remove the export before destroying:
exportfs -u *:/tank/data
For zvols held by a VM, shut the VM down cleanly rather than killing the hypervisor process; a killed qemu process can leave the device in a state that still reports busy. When every diagnostic comes up empty and the reference is a stale kernel artifact (lazy unmount leftovers, a dead namespace), a reboot clears it.
Prevention
- Track holds like snapshots. Holds are invisible in
zfs list. A periodic audit ofzfs holds -r <pool>catches accumulation from tooling before you hit EBUSY during an emergency cleanup. - Alert on snapshot space and count. Runaway snapshot retention is what turns a routine destroy into a dependency maze. Watch
usedbysnapshotsper dataset and the total snapshot count; the failure almost always starts as a pruning job that silently stopped. - Name hold tags after their owner. When every tool uses a distinct tag,
zfs holdsoutput is self-documenting and you know exactly who to ask before releasing. - Treat clones as temporary by default. Give clones created for tests and templates an explicit owner and expiry. Long-lived clones should be promoted deliberately or replaced with send/recv copies.
- Keep destroys out of capacity emergencies. Destroying snapshots on a nearly full pool competes with foreground I/O for async reclaim. Prune on a schedule so the destroy path is never your first response to a full pool.
How Netdata helps
- Netdata’s ZFS collectors chart pool capacity, per-dataset usage, and snapshot space over time, so you see snapshot and clone accumulation weeks before a destroy fails during a capacity incident.
- The pool
freeingtrend shows whether async reclaim after destroys is draining or backing up, which distinguishes “destroy is slow” from “destroy never happened”. - Correlating snapshot count growth with pool capacity growth on the same dashboard tells you whether a capacity problem is live data or retention, which determines whether you will be destroying snapshots at all.
- Alerting on capacity growth rate gives you runway to clean up clones and holds calmly, instead of discovering them at 3 a.m. with a pool at 96%.
Related guides
- ZFS ARC hit ratio low: cache misses, cold caches, and working sets that outgrew RAM
- ZFS zfs_arc_max: capping the ARC without starving read performance
- ZFS ARC and the OOM killer: applications killed while the cache will not shrink fast enough
- ZFS ARC shrinking below c_max: reading memory pressure before latency hits
- ZFS ARC using all memory: the Linux default that eats your RAM
- ZFS capacity planning: runway estimation before the pool fills
- ZFS checksum errors (CKSUM): the definitive signal of silent corruption
- ZFS checksum errors on multiple devices: suspect RAM or the controller, not the disks
- ZFS deadman events: hung I/O and a stalled pool sync
- ZFS device UNAVAIL or REMOVED: a disk that fell off the bus
- ZFS dirty data throttling: the write delay that masquerades as slow disks
- How ZFS actually works in production: a mental model for operators






