Someone eventually runs zfs destroy in a hurry. Not because they’re reckless—because prod is loud, disks fill up at 2 a.m., and the dataset names all look like they were generated by a committee. ZFS is powerful, but it’s not your babysitter. It will happily delete exactly what you asked for, not what you meant.
This is the practical guide to staying out of the “wrong dataset” crater. We’ll walk through the real guardrails ZFS provides, the traps it doesn’t, and the repeatable checks that keep deletions boring.
What zfs destroy really does (and what it refuses to do)
zfs destroy removes a ZFS dataset: a filesystem, a volume (zvol), a snapshot, or a bookmark. That deletion is metadata-driven; ZFS doesn’t “wipe blocks” in the old-school sense. It just removes references, and space becomes available once nothing else references those blocks.
That sounds reversible. It mostly isn’t. ZFS does not ship with a “trash can” for datasets. If you didn’t take a snapshot or replicate elsewhere, “undo” is a time machine you forgot to build.
Destroy targets and flags you must respect
- Filesystem:
tank/app— has a mountpoint and can contain children. - Snapshot:
tank/app@2025-12-26— immutable record; deletion frees referenced blocks that no other snapshot/clone holds. - Volume (zvol):
tank/vm01— block device under/dev/zvol; often used by hypervisors. - Bookmark:
tank/app#bk1— send/receive optimization pointer; tiny, but has dependency meaning.
The flags are where safety either exists or dies:
-rdestroys children (filesystems, snapshots under a dataset) recursively. Great when intentional. Catastrophic when you guessed wrong.-Rdestroys dependents, including clones. This is the “I really mean it” button—and also the “I didn’t know clones existed” button.-fforces unmount when destroying a filesystem (where supported). If you’re using-fcasually, you’re already off the happy path.-n(dry-run) exists on some platforms/versions for some operations, but do not rely on it universally for destroy behavior. Your safe workflow should not hinge on one flag being present everywhere.
Here’s the uncomfortable truth: zfs destroy is not “dangerous” because it’s unpredictable. It’s dangerous because it’s perfectly predictable. If your selection of the target dataset is sloppy, ZFS will faithfully execute your sloppiness at wire speed.
One short joke, because we all need one: ZFS doesn’t have a “recycle bin,” it has “recycle regret.”
Why “wrong dataset” happens in real life
Most wrong-destroy incidents are not a single typo. They’re a chain of tiny assumptions:
- You assume mountpoint equals dataset identity. It doesn’t (mountpoints can be inherited, changed, or set to legacy).
- You assume “this host” owns the dataset. Maybe it’s imported elsewhere, or replicated, or managed by HA.
- You assume “no one uses it” because no one mentioned it. Meanwhile a container, hypervisor, or backup agent is quietly using it.
- You assume children are harmless. Then
-rwipes ten datasets you forgot existed. - You assume snapshots are backups. They’re not, unless you have a second copy.
Built-in guardrails: clones, holds, busy mounts, and permissions
ZFS does provide real guardrails. They’re not always there when you need them, and they don’t cover the “I pointed at the wrong thing” category. But understanding them changes your safety posture immediately.
1) Clone dependencies block snapshot deletion
If a snapshot has a clone, ZFS won’t let you destroy the snapshot unless you address the clone dependency (or use a dependent-destroy operation). This is one of the few times ZFS saves you from yourself.
But it’s also a trap: you might think you’re deleting “an old snapshot” to free space. ZFS refuses, you add -R to “make it work,” and suddenly you’ve also destroyed a clone that someone was using for testing, analytics, or—my personal favorite—“temporary” data that became permanent three quarters ago.
2) Holds: the closest thing to a safety catch
A snapshot hold prevents snapshot deletion until explicitly released. Holds are simple, cheap, and extremely effective for safety. They are also underused because they feel “extra.” In production, “extra” is the whole point.
3) Busy datasets and mounts can resist destruction
A mounted filesystem in use can fail to unmount cleanly. Depending on platform and options, ZFS may refuse to destroy it, or you may “fix” it with -f and unmount something an application still needs. Either outcome is informative.
If destroy fails because it’s busy, that’s not ZFS being annoying. That’s ZFS telling you, “Hey, a process is attached; maybe verify you’re not about to delete a live workload.” Listen.
4) Delegated permissions can save you from yourself (and also from speed)
ZFS supports delegation: you can allow a role to snapshot but not destroy; or destroy only within a subtree; or require elevated privileges for the “sharp knives.” Organizations that treat zfs destroy like rm -rf /—meaning, not everyone gets to do it—tend to have fewer career-limiting events.
5) Naming, ancestry, and the “children surprise”
ZFS datasets are hierarchical. Humans are not. Under pressure, people see tank/app and don’t notice tank/app/cache, tank/app/log, tank/app/old, and tank/app/jenkins. Then -r makes a clean sweep. ZFS is doing exactly what you asked.
Facts and history that explain today’s behavior
Some context points that make ZFS destroy behavior less mysterious and more predictable:
- ZFS originated at Sun Microsystems in the mid-2000s, designed to make storage management more like software than like ritual.
- Copy-on-write is the core mechanic: ZFS never overwrites live blocks in place. This makes snapshots cheap and makes deletions mostly about reference counts.
- Snapshots are not a separate store. They’re a point-in-time view; space is only freed when blocks become unreferenced by all snapshots/clones.
- Clones were designed for fast provisioning (think dev/test, VM templates). The safety consequence: snapshots can become “undeletable” until you deal with dependents.
- Early ZFS emphasized administrative clarity via properties and inheritance, which is great—until inherited properties make two datasets look identical at a glance.
- ZFS destroy is fast because it’s metadata-heavy. Fast deletion is a feature. The safety tax is you must slow yourself down.
- OpenZFS grew across multiple operating systems. Some flags/behaviors differ subtly by platform and version; portable runbooks must avoid assuming one exact CLI capability.
- The “zpool/zfs split” (pool vs dataset commands) reflects the architecture: pool operations are physical-ish, dataset operations are logical-ish. Wrong-dataset incidents usually happen at the logical layer.
Reliability engineering has a worldview for this. Here’s a paraphrased idea often attributed to James Hamilton (AWS): Operate with the assumption that failure is normal; build systems and processes that tolerate it.
That includes operator failure.
Fast diagnosis playbook (when you need space now)
This is the “I’m on-call, the pool is 95% full, and someone is about to suggest deleting something” sequence. The goal: find the real space consumer and confirm dataset identity before any destructive action.
First: confirm the pool health and actual pressure
- Check pool status (errors, degraded, resilvering can distort expectations).
- Check logical vs physical usage (compression, snapshots, special vdevs can change the story).
- Identify top datasets by used space and separate “live data” from “snapshot-held data.”
Second: locate “space held by snapshots” versus “space used by current head”
- Use
usedbysnapshotsat dataset level. - List snapshots ordered by used if available.
- Check for clones/holds that will block deletion or make it risky.
Third: validate the dataset is the one you intend
- Confirm mountpoint and where it’s mounted (including legacy mounts).
- Confirm it’s not a zvol backing a VM (look for consumers).
- Confirm it’s not receiving replication (destroying a receive target midstream is an easy way to create a long night).
If you follow only one rule: never decide “what to destroy” based purely on a path like /srv/app. Decide based on dataset name and properties, then map to paths.
Practical tasks: commands, outputs, and decisions
These are real tasks you can run in production. Each includes a sample output and the decision you should make from it. Adjust pool/dataset names to your environment.
Task 1: Verify the pool is healthy before you touch anything
cr0x@server:~$ zpool status -x
all pools are healthy
What it means: No known pool-level failures right now.
Decision: Proceed with diagnosis normally. If this shows degraded/faulted, stop and handle hardware/resilvering first; deleting data during a failing pool event is how you turn “tight” into “irrecoverable.”
Task 2: Get a fast “what is using space” view across datasets
cr0x@server:~$ zfs list -o name,used,available,refer,mountpoint -S used tank
NAME USED AVAIL REFER MOUNTPOINT
tank/backup 7.12T 1.80T 7.12T /tank/backup
tank/app 1.04T 1.80T 120G /srv/app
tank/app/log 320G 1.80T 320G /srv/app/log
tank/app/cache 210G 1.80T 210G /srv/app/cache
tank/home 96G 1.80T 96G /home
What it means: USED includes snapshots/children; REFER is the dataset’s current head (excluding children).
Decision: If USED is huge but REFER is small, snapshots/children are likely the story. Don’t destroy “the dataset” to fix “snapshot bloat.”
Task 3: Separate snapshot-held space from live data
cr0x@server:~$ zfs get -o name,property,value -H used,usedbysnapshots,usedbychildren tank/app
tank/app used 1.04T
tank/app usedbysnapshots 880G
tank/app usedbychildren 40G
What it means: Most space is held by snapshots (usedbysnapshots), not the live filesystem.
Decision: Review snapshot policy and delete specific snapshots (carefully) rather than destroying the dataset. If you destroy the dataset, you might delete live data plus its descendants. Overkill is still kill.
Task 4: List snapshots and see what you’re actually about to touch
cr0x@server:~$ zfs list -t snapshot -o name,used,refer,creation -S used tank/app
NAME USED REFER CREATION
tank/app@hourly-2025-12-26 22G 120G Fri Dec 26 02:00 2025
tank/app@hourly-2025-12-25 21G 118G Thu Dec 25 23:00 2025
tank/app@daily-2025-12-20 19G 110G Sat Dec 20 01:00 2025
What it means: Snapshot USED is unique space held by that snapshot.
Decision: Delete snapshots with a known retention policy. If you can’t explain why a snapshot exists, assume it exists for a reason and find that reason before deleting.
Task 5: Check for snapshot holds (your “do not delete” tag)
cr0x@server:~$ zfs holds tank/app@daily-2025-12-20
NAME TAG TIMESTAMP
tank/app@daily-2025-12-20 legal Wed Dec 24 10:13 2025
What it means: A hold named legal prevents destruction of this snapshot.
Decision: Stop. This is governance or operational intent made explicit. Find the owner and get approval before releasing holds.
Task 6: Check for clones that make snapshot deletion risky
cr0x@server:~$ zfs get -H -o value clones tank/app@daily-2025-12-20
tank/devclone
What it means: Snapshot has a dependent clone tank/devclone.
Decision: Do not use -R as a reflex. Confirm whether tank/devclone is in use, and whether you can promote it or snapshot it before any deletion chain.
Task 7: Confirm dataset identity and ancestry before any destroy
cr0x@server:~$ zfs list -r -o name,mountpoint,canmount,readonly tank/app
NAME MOUNTPOINT CANMOUNT RDONLY
tank/app /srv/app on off
tank/app/cache /srv/app/cache on off
tank/app/log /srv/app/log on off
What it means: This is the exact subtree you’ll affect with -r. No guessing.
Decision: If you intend to delete only logs, target tank/app/log, not tank/app. If you intended to delete the whole app, make sure every child is expected.
Task 8: Map a mountpoint back to a dataset (stop deleting by path)
cr0x@server:~$ findmnt -T /srv/app -o TARGET,SOURCE,FSTYPE,OPTIONS
TARGET SOURCE FSTYPE OPTIONS
/srv/app tank/app zfs rw,xattr,noacl
What it means: The filesystem mounted at /srv/app is tank/app.
Decision: Use the dataset name in all commands and approvals. If SOURCE is not what you expected, stop and investigate. Paths lie; ZFS names don’t.
Task 9: Check if a dataset is in active use (before forcing unmount)
cr0x@server:~$ lsof +f -- /srv/app | head
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 1421 root cwd DIR 0,105 4096 2 /srv/app
java 1880 app txt REG 0,105 12345678 7 /srv/app/bin/app.jar
What it means: Processes are using files under this mount.
Decision: Don’t destroy or force-unmount. Coordinate an application stop, or target snapshots instead of the live filesystem.
Task 10: Confirm whether you’re dealing with a zvol (VM risk)
cr0x@server:~$ zfs list -t volume -o name,used,volsize,readonly tank | head
NAME USED VOLSIZE RDONLY
tank/vm01 220G 256G off
tank/vm02 180G 256G off
What it means: These datasets are block devices. Destroying them is instant VM amputation.
Decision: Require an explicit VM ownership check before any destroy. Treat volumes as higher-risk than filesystems because the consumer is often a hypervisor, not a human.
Task 11: Check dataset properties that change how “obvious” it is
cr0x@server:~$ zfs get -o name,property,value -H mountpoint,canmount,origin,receive_resume_token tank/app
tank/app mountpoint /srv/app
tank/app canmount on
tank/app origin -
tank/app receive_resume_token -
What it means: Not a clone (origin is -), and not mid-resume of a receive.
Decision: If origin is set, understand the clone lineage before deletion. If receive_resume_token is present, replication is/was in progress—destroying may complicate recovery or future incremental sends.
Task 12: Use zfs destroy on a snapshot (narrowly), not a dataset (broadly)
cr0x@server:~$ zfs destroy tank/app@hourly-2025-12-25
What it means: Snapshot removed (if no holds/clones block it). No output on success is typical.
Decision: Prefer deleting specific snapshots to free space rather than deleting whole datasets. You’re shaving, not decapitating.
Task 13: Verify that deletion actually returned space (and whether it could)
cr0x@server:~$ zfs get -o name,property,value -H usedbysnapshots tank/app
tank/app usedbysnapshots 840G
What it means: Snapshot-held space dropped from 880G to 840G in our earlier example, so space is freeing as expected.
Decision: If space doesn’t drop, you likely deleted snapshots that didn’t hold much unique data, or clones/other snapshots still reference the blocks. Keep diagnosing rather than “try more destroys.”
Task 14: Check which snapshots are actually holding space (dataset-level view)
cr0x@server:~$ zfs list -t snapshot -o name,used -S used -r tank/app | head
NAME USED
tank/app@hourly-2025-12-26 22G
tank/app@hourly-2025-12-24 22G
tank/app@hourly-2025-12-23 21G
tank/app@daily-2025-12-20 19G
What it means: The biggest unique snapshot deltas are at the top.
Decision: Delete in descending “used” order only if it matches policy and business needs. If a snapshot is big, it might be big because it captured a major deployment.
Task 15: Find “hidden” datasets via legacy mounts
cr0x@server:~$ zfs get -o name,property,value -H mountpoint tank | grep legacy
tank/oldapps mountpoint legacy
What it means: This dataset’s mountpoint isn’t managed by ZFS; it’s likely mounted via /etc/fstab or a service unit.
Decision: Don’t assume it’s unused because it isn’t mounted where you expect. Check system mounts and consumers before any deletion.
Task 16: Confirm the exact dataset string you’re about to destroy (human-proofing)
cr0x@server:~$ zfs list -H -o name tank/app
tank/app
What it means: Dataset exists and you have the canonical name with no formatting noise.
Decision: Copy-paste this exact output into your change record and into your command line. Typing is how you create alternate realities.
Second short joke (and then we get back to work): The only thing faster than zfs destroy is the meeting you get scheduled after it.
Three corporate mini-stories from the deletion trenches
Mini-story 1: The incident caused by a wrong assumption
The team had a standard pattern: application files lived under /srv/app, logs under /srv/app/log, cache under /srv/app/cache. Someone rotated into on-call and inherited the mental model: “If the path exists, it must be the dataset.” That assumption held for years—right up until a migration.
During the migration, a storage engineer moved tank/app/log onto a separate pool for I/O isolation and used a bind mount to preserve paths. The ZFS dataset name changed; the mountpoint did not. The on-call engineer got paged for low space and followed an old runbook: “destroy old logs dataset.” They ran zfs destroy -r tank/app, believing it would affect only the log subtree they cared about.
It did what the command says. tank/app was the live application dataset, and -r walked the children. The bind-mounted logs dataset survived (different pool), but the application binaries, configs, and uploaded customer assets did not. Monitoring showed the app flapping; the load balancer took it out; incident declared.
The postmortem wasn’t about “operator error.” It was about an organization that let a path-based runbook drift away from dataset reality. The fix was boring: every runbook step that mentioned a path had to include the dataset name verification via findmnt and zfs list. They also enforced “no -r without listing children in the ticket.” The next on-call hated it—until the next near-miss.
Mini-story 2: The optimization that backfired
A platform team wanted faster CI environments. Clones were the obvious play: take a golden snapshot of a build cache dataset, clone per job, destroy at the end. It was elegant. It was also a slow-burn storage leak disguised as success.
The initial design cloned from tank/ci/cache@golden and set a short TTL. But the cleanup job was tied to the CI scheduler’s “job finished” hook. When the scheduler had a bad day (network partition, control-plane restart), cleanup didn’t run. Clones piled up. Everyone still had fast CI, which meant nobody looked closely at storage growth.
Then the pool hit high utilization, and performance got weird. Allocation started fragmenting; synchronous writes slowed; latency spikes showed up in unrelated workloads. The on-call saw the biggest consumer: tank/ci/cache. They tried deleting old snapshots to free space. ZFS refused. The snapshot had clones. “Fix it” became “use -R.”
-R did free space. It also deleted active clones belonging to in-flight jobs, triggering build failures and a chain reaction of retries. The pool recovered; CI became a storm; the control plane fell over again. The optimization had turned into a feedback loop.
The long-term fix wasn’t “don’t use clones.” It was “treat clone lifecycles as production data.” They added holds for the golden snapshot, a periodic reconciler that destroys expired clones based on a ZFS property, and a guardrail: no one could run zfs destroy -R in CI namespaces without a second approval.
Mini-story 3: The boring but correct practice that saved the day
A finance-facing service stored invoices on ZFS. The team did one unsexy thing right: they used snapshot holds for compliance snapshots and replicated them off-host. Every month-end snapshot got a hold tag and a ticket reference. It was bureaucratic. It also worked.
One quarter, a deployment accidentally wrote a flood of bad data—duplicates, corrupted PDFs, the works. The incident response initially focused on code rollback, but the data was already polluted. Someone suggested: “Destroy the dataset and restore from backup.” That’s the kind of suggestion that sounds decisive right up until you realize “backup” is a process, not a noun.
Instead, they used the held monthly snapshot as the trust anchor. They created a clone of the snapshot for forensic analysis while keeping the original immutable, then used replication to restore a clean dataset to a new namespace. The old polluted dataset stayed in place long enough to extract what was still useful, then it was destroyed deliberately with sign-off.
No heroics. No -R panic. The key was that the “boring” safety mechanism—holds + off-host replication—meant the team could move slowly even when the incident was fast.
Common mistakes: symptom → root cause → fix
1) “I deleted snapshots but the pool didn’t free space”
Symptom: You destroy multiple snapshots; zpool list barely changes.
Root cause: Blocks are still referenced by other snapshots, clones, or dataset head; the deleted snapshots didn’t hold unique data.
Fix: Inspect snapshot USED and clone dependencies. Use zfs list -t snapshot -o name,used -S used and check zfs get clones on large/old snapshots. Delete high-unique snapshots that match retention policy, not random ones.
2) “Destroy failed: dataset is busy”
Symptom: cannot unmount / dataset is busy errors.
Root cause: Processes hold open files; NFS exports; container runtimes; systemd mount dependencies.
Fix: Identify consumers with lsof/fuser, stop services cleanly, and only then destroy. If you’re tempted by -f, treat it as a change request, not a shell impulse.
3) “I destroyed a dataset but the directory still exists”
Symptom: Path is still there; you assume destruction didn’t work.
Root cause: Mountpoint directory is just a directory; the dataset was providing a mounted filesystem on top of it. After unmount/destroy, the underlying directory remains.
Fix: Verify with findmnt -T and zfs list. Remove or repurpose the directory intentionally; don’t treat it as evidence of failure.
4) “I destroyed a snapshot and now replication increments broke”
Symptom: Next incremental send fails because the expected base snapshot is missing.
Root cause: Replication tooling depended on a specific snapshot naming chain; deletion broke the chain.
Fix: Align snapshot retention with replication requirements. Preserve replication anchor snapshots via holds or distinct retention classes. Don’t delete “random old” snapshots on replicated datasets.
5) “I ran destroy -r and deleted more than I meant”
Symptom: Unexpected datasets disappeared; mountpoints vanished; services failed.
Root cause: You didn’t inventory children, or you assumed children were “just directories.” They were datasets.
Fix: Always run zfs list -r and paste output into the change record before using -r. If the subtree surprises you, stop and re-scope.
6) “Destroy succeeded, but users still see data”
Symptom: After destroying a dataset, the path still serves content.
Root cause: You destroyed a dataset with the same mountpoint as another dataset, or you have an automounter/remount, or a different host is serving it (NFS/SMB/cluster).
Fix: Confirm actual mount sources (findmnt), confirm exports, and verify you’re on the host that serves the path. ZFS naming is per-host; user experience might be coming from elsewhere.
Checklists / step-by-step plan for safe destruction
Checklist A: Before you destroy anything (the identity checks)
- Get the dataset name from the system, not your brain. Use
zfs list -H -o nameorfindmntmapping. - Confirm the subtree. Run
zfs list -rand review children. - Confirm mount behavior. Check
mountpoint,canmount,readonly, andlegacystatus. - Confirm consumers. Use
lsofon mountpoints; for zvols confirm VM/hypervisor ownership. - Check snapshot holds and clones. Holds mean “stop.” Clones mean “understand dependents.”
- Confirm replication/receive state. Look for receive resume tokens or known replication schedules; don’t break incremental chains casually.
Checklist B: If the goal is “free space,” prefer these actions in order
- Delete obvious junk inside the filesystem (application-level cleanup) if safe.
- Delete snapshots that match a documented retention policy, starting with those with highest unique
USED. - Move data out (replicate, archive) and then destroy a now-empty dataset.
- Destroy a whole dataset only when the dataset is truly decommissioned, not merely “large.”
Checklist C: If you must destroy a dataset in production
- Take a final snapshot (and replicate it off-host if you can). If the dataset is already known-bad (corruption at app layer), snapshot anyway for forensic rollback.
- Put a short-lived hold on that final snapshot so nobody “helpfully” deletes it during the change window.
- Disable automounters/services that might remount or recreate the dataset path.
- Destroy the narrowest target. Don’t use
-runless you intend the full subtree. Don’t use-Runless you intend to kill clones. - Verify removal with
zfs list, verify mounts, and verify application behavior. - Watch pool space and errors for at least one monitoring interval after the change.
Automation safety rails that don’t make you slower
Human safety practices are necessary. They are not sufficient. Production deletes eventually happen via automation: deprovision pipelines, tenant cleanup, CI tear-down, environment resets. You need rails that assume the operator is tired and the script is literal.
1) Require an explicit dataset allowlist pattern
Automation should only destroy datasets in a known namespace, like tank/ci/* or tank/ephemeral/*. If a dataset name falls outside that prefix, the script should refuse.
2) Enforce “list subtree before recursive destroy”
Before any -r, scripts should print zfs list -r output and require a human confirmation in interactive mode, or store it as an artifact in non-interactive mode. This gives you forensic evidence later and prevents silent wipes.
3) Use holds as a policy boundary
Automation should refuse to destroy any snapshot with holds, and should be cautious with datasets that contain held snapshots. Holds are a cheap, built-in “stop sign.” Treat them that way.
4) Tag datasets with properties and make decisions from properties
Set a custom property like com.example:purpose=ci or com.example:ttl=2025-12-27T00:00Z (naming varies by org). Then your cleanup job destroys only datasets whose properties say they are disposable. Names are helpful. Properties are enforceable.
5) Limit who can destroy in the first place
Use delegation so most roles can snapshot/clone but not destroy outside their subtree. It’s not about trust; it’s about reducing blast radius. In practice, this also forces destroy operations into reviewable workflows.
FAQ
1) Does ZFS have a built-in undelete for destroyed datasets?
No. If you destroyed a dataset and have no snapshots and no replicated copy, recovery is not something you should plan on. Treat destroy as permanent.
2) Is deleting a snapshot “safe” compared to deleting a dataset?
Safer, yes, because it’s narrower. Still not automatically safe: snapshots might be replication anchors, compliance artifacts, or required for rollback. Check holds, clone dependencies, and your backup/replication workflow.
3) What’s the difference between -r and -R on zfs destroy?
-r destroys child datasets and snapshots under the target dataset. -R destroys dependents, including clones, and implies recursion in a much more destructive way. Use -R only when you have explicitly enumerated what it will take with it.
4) Why does ZFS refuse to destroy a snapshot sometimes?
The common reasons are: the snapshot has a hold, the snapshot has dependent clones, or you lack permission. ZFS refuses because deletion would violate dependency rules or policy.
5) Can I rely on mountpoints to identify datasets?
Not reliably. Mountpoints can be inherited, changed, set to legacy, or shadowed by other mounts. Always map paths to datasets with mount inspection tools, then operate on dataset names.
6) If I destroy a dataset, do snapshots of it also disappear?
Yes, if you destroy the dataset (and especially with recursion), you are typically removing the dataset and its snapshots in that scope. If you want to keep a recovery point, take and replicate a snapshot elsewhere first—then destroy.
7) Why didn’t deleting a big snapshot free as much space as its size?
Snapshot “size” is not a simple number. The snapshot’s USED value is the unique space held by that snapshot, not the total data view. If other snapshots or clones reference the same blocks, space won’t free until all references are gone.
8) Is it okay to use zfs destroy -f to force it through?
Only when you’ve confirmed the dataset is supposed to die and you’ve identified why it’s busy. Forcing unmount can break running applications and can hide a wrong-target mistake. Treat -f as an escalation step with explicit verification.
9) What’s the safest operational pattern for dataset decommissioning?
Snapshot → replicate (or otherwise ensure off-host copy) → hold the final snapshot temporarily → disable services/mounts → destroy narrowly → verify. If you can’t replicate, at least snapshot and hold for a defined window.
10) How do I prevent a script from destroying the wrong dataset?
Use a strict namespace allowlist, validate dataset existence and subtree, refuse targets with holds, and require properties that mark the dataset as disposable. Make the script annoying in the right ways.
Next steps you can implement this week
If you run ZFS in production, you can make wrong-dataset destroys rarer without slowing the team to a crawl.
- Update your runbooks: every destructive step must include “map path to dataset” and “list subtree” checks.
- Adopt snapshot holds for “must-keep” snapshots: compliance, month-end, pre-migration, pre-upgrade. Holds are cheap insurance.
- Establish a naming + namespace convention: disposable datasets live under a prefix that automation can enforce.
- Restrict destroy privileges: most humans don’t need them. Delegate snapshot/clone widely; restrict destroy narrowly.
- Practice the space playbook: teach on-call to distinguish
refervsusedbysnapshotsso they stop deleting “big datasets” to solve “big snapshots.” - Make
-Rsocially expensive: require listing clone dependencies and a second set of eyes. This alone prevents a depressing number of avoidable outages.
ZFS is a scalpel. It can also be a chainsaw, but only if you insist on gripping it by the wrong end. The guardrails exist; your job is to use them on purpose.