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

CauseWhat it looks likeFirst 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 destroyzfs holds -r <pool> or the userrefs property
Dataset still mounted or in use“dataset is busy” on a filesystem destroyzfs get mounted, then mount tables
Mount inside a container namespaceHost shows nothing mounted, destroy still failsgrep <dataset> /proc/*/mountinfo
Lazy unmount left a referenceMountpoint gone from /proc/mounts, still busyRecent umount -l in shell history or scripts
NFS export holds the datasetNo local mount, still busyexportfs -v
VM process holds a zvol open“dataset is busy” on a zvol destroyfuser -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]
  1. 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.

  2. Map the clone dependency. Run zfs get clones <snapshot> to see exactly which clones depend on the snapshot, and zfs 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?

  3. Enumerate holds. Run zfs holds -r <pool> (recursive, so you see holds anywhere in the hierarchy) and zfs get userrefs <snapshot>. A non-zero userrefs with no output from zfs holds on that snapshot usually means the holds are on a different snapshot in the chain or you did not use -r.

  4. Hunt hidden mounts. If the object is a filesystem and zfs get mounted says no (or you already unmounted it), check mount namespaces with grep -l "<dataset>" /proc/*/mountinfo. If a PID comes back, that process has the dataset mounted inside its own namespace. For zvols, check fuser -am /dev/zvol/<pool>/<name> for hypervisor processes holding the device open.

  5. Check exports and lazy unmounts. exportfs -v shows NFS exports, which hold a reference even with no client connected. If someone ran umount -l earlier, 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.

  6. Resolve, then retry the destroy. Apply the matching fix below, then re-run the same zfs destroy command. Do not escalate to -R until you know exactly what the dependents are.

Metrics and signals to monitor

SignalWhy it mattersWarning sign
usedbysnapshots per datasetClone and snapshot chains pin space you think you deletedSnapshot space dominating a dataset’s usage
userrefs on snapshotsReveals hold accumulation from backup toolingHolds with no owner you can identify
Pool freeing propertyShows async reclaim in progress after destroysLarge freeing backlog that never drains
Snapshot count and ageRetention jobs that create but never prune lead to this errorSnapshot count growing monotonically week over week
Clone count per poolEvery clone is a future “dependent clones” errorClones 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 of zfs 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 usedbysnapshots per 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 holds output 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 freeing trend 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%.