Loading content...
Traditional backup has an uncomfortable trade-off: capturing consistent state versus system availability. Backing up a running database risks capturing inconsistent data mid-transaction. Stopping the database ensures consistency but means downtime.
Even sophisticated backup solutions—VSS on Windows, LVM snapshots on Linux—impose measurable overhead. They require planning storage for copy-on-write areas, and if that area fills, the snapshot fails catastrophically.
ZFS snapshots change the equation fundamentally. They're instant (milliseconds regardless of dataset size), free until data changes, and require no pre-planning or reserved space. They're so lightweight that taking one per minute is entirely practical.
By the end of this page, you will understand how Copy-on-Write enables instant snapshots, the difference between snapshots and clones, practical commands for snapshot management, how to use snapshots for backups and replication, clone workflows for development and testing, and best practices for snapshot retention and performance.
Traditional file systems modify data in place. A backup must physically copy every block before it can be changed, or risk capturing inconsistent state. This is slow and storage-intensive.
ZFS never modifies data in place. When you modify a file, ZFS:
A snapshot is simply a saved pointer to a past state. Creating a snapshot doesn't copy any data—it just marks the current block pointers as "don't free these yet."
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
HOW ZFS SNAPSHOTS WORK═══════════════════════════════════════════════════════════════════ INITIAL STATE (No snapshot):─────────────────────────────────────────────────────────────────Dataset root points to current data. When data changes,old blocks are freed immediately. Dataset: tank/data │ │ (current pointer) ▼ ┌───────────────┐ │ Current State │ │ Block A │ │ Block B │ │ Block C │ └───────────────┘ Modify file: Block B → Block B' Dataset: tank/data │ │ (points to new) ▼ ┌───────────────┐ │ Current State │ │ Block A │ │ Block B' ←─── │ (NEW block) │ Block C │ └───────────────┘ Block B → FREED (recycled for future use) ═══════════════════════════════════════════════════════════════════WITH SNAPSHOT:═══════════════════════════════════════════════════════════════════ Step 1: Create snapshot zfs snapshot tank/data@before-change Snapshot is nearly instantaneous - just saves current pointer Dataset: tank/data Snapshot: tank/data@before-change │ │ │ │ ▼ ▼ ┌───────────────┐ ┌───────────────┐ │ Current State │ ══════ │ Same Blocks! │ │ Block A │ │ Block A │ │ Block B │ │ Block B │ │ Block C │ │ Block C │ └───────────────┘ └───────────────┘ Both pointers reference THE SAME blocks. No data was copied. Snapshot is FREE (zero additional space). Step 2: Modify file (Block B → Block B') Dataset: tank/data Snapshot: tank/data@before-change │ │ │ │ ▼ ▼ ┌───────────────┐ ┌───────────────┐ │ │ │ │ │ Block A ═════════════════ │ Block A │ (SHARED) │ │ │ │ │ Block B' ←────│──────── │ Block B │ (DIVERGED) │ │ │ │ │ Block C ═════════════════ │ Block C │ (SHARED) │ │ │ │ └───────────────┘ └───────────────┘ Block B' is NEW (written to new location). Block B is KEPT (snapshot still references it). Blocks A and C are SHARED (referenced by both). Step 3: Space accounting Before change: Dataset uses 30GB, Snapshot uses 0GB (shared) After change: Dataset uses 31GB, Snapshot uses 1GB (Block B retained) Total pool usage increased by 1GB (Block B' size), NOT 31GB! ═══════════════════════════════════════════════════════════════════WHY THIS IS REVOLUTIONARY:───────────────────────────────────────────────────────────────── 1. INSTANT: Creating snapshot = saving a pointer (nanoseconds)2. FREE: No data copied = no space used until data changes3. LIVE: Dataset remains fully writable during snapshot4. CHEAP: Only changed blocks since snapshot consume space5. ATOMIC: Point-in-time consistency guaranteed6. UNLIMITED: No pre-reserved space needed; grows dynamicallyThink of snapshots not as 'copies' but as 'checkpoints'. When you snapshot, you're not duplicating data—you're telling ZFS 'remember what things looked like at this moment.' ZFS then diverges from that checkpoint as you make changes, but the checkpoint remains accessible.
ZFS provides straightforward commands for snapshot management. Snapshots are named with the format dataset@snapshotname.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
#!/bin/bash# ZFS Snapshot Management # ============================================# CREATING SNAPSHOTS# ============================================ # Create a simple snapshotzfs snapshot tank/data@backup-2024-01-15 # Create snapshot with timestamp (common pattern)zfs snapshot "tank/data@auto-$(date +%Y-%m-%d_%H-%M-%S)" # Create recursive snapshot (dataset and all children)zfs snapshot -r tank@full-backup# Creates:# tank@full-backup# tank/home@full-backup # tank/data@full-backup# etc. # ============================================# LISTING SNAPSHOTS# ============================================ # List all snapshotszfs list -t snapshot # List snapshots for specific datasetzfs list -t snapshot -r tank/data # List snapshots with creation timezfs list -t snapshot -o name,creation tank/data # List snapshots with used spacezfs list -t snapshot -o name,used,referenced tank/data # ============================================# ACCESSING SNAPSHOT CONTENTS# ============================================ # Method 1: The .zfs hidden directory (automatically available)ls /tank/data/.zfs/snapshot/# Output: backup-2024-01-15 auto-2024-01-14_23-00-00 # Access files from snapshot (read-only)cat /tank/data/.zfs/snapshot/backup-2024-01-15/myfile.txt # Copy file from snapshot to active datasetcp /tank/data/.zfs/snapshot/backup-2024-01-15/deleted-file.txt /tank/data/ # Make .zfs directory visible in listings (optional)zfs set snapdir=visible tank/datals -la /tank/data/# Now shows: .zfs # Method 2: Clone the snapshot (covered later)# Method 3: Mount the snapshot directly # Mount snapshot as read-only filesystemmkdir /mnt/snapshot-viewmount -t zfs tank/data@backup-2024-01-15 /mnt/snapshot-view # ============================================# ROLLING BACK TO A SNAPSHOT# ============================================ # WARNING: Rollback DESTROYS all data created after the snapshot! # Return dataset to exact state at snapshot timezfs rollback tank/data@backup-2024-01-15 # If there are newer snapshots, must destroy them first or use -rzfs rollback -r tank/data@backup-2024-01-15# This destroys all snapshots between the target and current # Rollback with clones (very destructive)zfs rollback -R tank/data@backup-2024-01-15# Destroys clones depending on destroyed snapshots # ============================================# COMPARING SNAPSHOTS# ============================================ # Show what changed since a snapshotzfs diff tank/data@backup-2024-01-15# Output format:# M /tank/data/modified-file.txt# + /tank/data/new-file.txt# - /tank/data/deleted-file.txt# R /tank/data/old-name.txt -> /tank/data/new-name.txt # Compare two snapshotszfs diff tank/data@snapshot-1 tank/data@snapshot-2 # ============================================# DESTROYING SNAPSHOTS# ============================================ # Destroy a single snapshotzfs destroy tank/data@backup-2024-01-15 # Destroy snapshots recursivelyzfs destroy -r tank@full-backup # Destroy a range of snapshotszfs destroy tank/data@daily-%-daily-%# Destroys all snapshots matching the pattern # Destroy with dry-run (preview what would be destroyed)zfs destroy -nv tank/data@old-snapshot # ============================================# SNAPSHOT HOLDS (Prevent Destruction)# ============================================ # Place a hold on a snapshot (prevents accidental destruction)zfs hold keep-this tank/data@important-backup # List holdszfs holds tank/data@important-backup # Release a holdzfs release keep-this tank/data@important-backup # Attempt to destroy held snapshot failszfs destroy tank/data@important-backup# Error: cannot destroy: dataset is busyRolling back to a snapshot DESTROYS all changes made since that snapshot. It's not like 'undo' where you can redo—the post-snapshot data is gone. For non-destructive recovery, use 'cp' from the snapshot directory or create a clone instead of rolling back.
Understanding snapshot space consumption is essential for capacity planning. Snapshots consume space only for blocks that would otherwise be freed—blocks that have changed since the snapshot.
| Property | Meaning | Use Case |
|---|---|---|
| used | Space uniquely held by this snapshot | Destroying this snapshot frees this space |
| referenced | Space reachable from this snapshot | Total 'size' of the snapshot's view of data |
| written | Space written since previous snapshot | Change rate analysis |
| usedbysnapshots | Sum of space used by all snapshots (dataset property) | Total snapshot overhead for dataset |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
UNDERSTANDING SNAPSHOT SPACE USAGE═══════════════════════════════════════════════════════════════════ $ zfs list -t snapshot -o name,used,refer,written tank/dataNAME USED REFER WRITTENtank/data@snap1 100M 50G 100Mtank/data@snap2 150M 52G 150Mtank/data@snap3 0B 53G 1G INTERPRETATION:───────────────────────────────────────────────────────────────── snap1 (oldest): used=100M: snap1 holds 100M of unique data that would be freed if snap1 were destroyed. This is data that existed at snap1 time but has since been deleted or modified. refer=50G: From snap1's perspective, the dataset totals 50G. This includes shared blocks still in the live dataset. written=100M: (Same as used for oldest snapshot) snap2 (middle): used=150M: snap2 holds 150M uniquely. Destroying snap2 frees 150M. refer=52G: Dataset grew by 2G between snap1 and snap2. written=150M: Between snap1 and snap2, 150M of data was written (new files + modifications). snap3 (newest): used=0B: snap3 has NO unique data! Everything it references is either still in the live dataset or shared with older snapshots. refer=53G: Current total reachable from snap3. written=1G: 1GB written since snap2, but it's not yet "unique" to snap3 because the live dataset still has it. ═══════════════════════════════════════════════════════════════════ CRITICAL INSIGHT: SNAPSHOT SPACE IS CUMULATIVE───────────────────────────────────────────────────────────────── Scenario: Data modified multiple times with snapshots Time T1: Create snap1, Block A contains "Version 1"Time T2: Modify Block A to "Version 2", create snap2Time T3: Modify Block A to "Version 3", create snap3Time T4: Modify Block A to "Version 4" (current) Storage used: - Block A "Version 1" → held by snap1 (100MB) - Block A "Version 2" → held by snap2 (100MB) - Block A "Version 3" → held by snap3 (100MB) - Block A "Version 4" → live dataset (100MB) Total for one file: 400MB (4 versions) To free snap2's space: - Destroy snap2: "Version 2" freed → saves 100MB - But "Version 1" is still in snap1 - And "Version 3" is still in snap3 To free ALL snapshot space: - Destroy all snapshots - Only "Version 4" (current) remains ═══════════════════════════════════════════════════════════════════ SPACE RECOVERY STRATEGY:───────────────────────────────────────────────────────────────── Q: Pool is at 95% capacity. How to identify space to recover? $ zfs list -o name,used,usedbysnapshots tank/dataNAME USED USEDBYSNAPSHOTStank/data 800G 250G → Snapshots account for 250G. Destroying some would free space. $ zfs list -t snapshot -o name,used -s used tank/data | tail -10→ Shows snapshots sorted by unique space used. Destroy oldest/largest-used snapshots first for maximum recovery.Implement automated snapshot retention: keep hourly snapshots for 24 hours, daily snapshots for 30 days, weekly snapshots for 6 months, monthly snapshots for years. Tools like 'zfs-auto-snapshot', 'sanoid', or 'zrepl' automate this policy, creating and destroying snapshots on schedule.
A clone is a writable copy of a snapshot. Like snapshots, clones share data with their parent through Copy-on-Write—they're instant to create and initially consume zero space. Unlike snapshots, clones allow modifications.
Clones are full datasets — they appear and behave exactly like any other ZFS dataset, with their own properties, snapshots, and mountpoint. The only difference is that they share blocks with their parent snapshot.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
#!/bin/bash# ZFS Clone Management # ============================================# CREATING CLONES# ============================================ # First, create a snapshot (clones are created FROM snapshots)zfs snapshot tank/production@clone-source # Create a clone from the snapshotzfs clone tank/production@clone-source tank/development # Clone is immediately available, mounted, and writablels /tank/development/# Contains same data as /tank/production at snapshot time # Create clone with custom mountpointzfs clone -o mountpoint=/var/www/staging \ tank/production@clone-source tank/staging # ============================================# CLONE PROPERTIES AND ORIGIN# ============================================ # A clone knows its origin snapshotzfs get origin tank/development# NAME PROPERTY VALUE SOURCE# tank/development origin tank/production@clone-source - # List all clones of a snapshotzfs list -t all -o name,origin | grep clone-source # Show clone dependencieszfs list -o name,origin -r tank # ============================================# WORKING WITH CLONES# ============================================ # Clones behave exactly like regular datasetszfs set compression=lz4 tank/developmentzfs snapshot tank/development@dev-checkpointzfs set quota=100G tank/development # Modifications only consume space for changed blocks# Initially, clone uses 0 space (all shared with origin)zfs list -o name,used,refer tank/development# NAME USED REFER# tank/development 0 500G ← 0 used, 500G shared # After modifications:zfs list -o name,used,refer tank/development# NAME USED REFER # tank/development 2G 502G ← 2G of changes made # ============================================# CLONE USE CASES# ============================================ # 1. DEVELOPMENT/TESTING ENVIRONMENTS# Clone production to development without copying datazfs snapshot tank/production@2024-01-15zfs clone tank/production@2024-01-15 tank/dev-env# Developer experiments freely; production unaffected # 2. DATABASE TESTING# Test database migrations on clone before productionzfs snapshot tank/postgres@pre-migrationzfs clone tank/postgres@pre-migration tank/postgres-test# Run migration on test, verify results# If good: apply to production# If bad: destroy clone, production untouchedzfs destroy tank/postgres-test # 3. QUICK RECOVERY TESTING# Test that backup (snapshot) is actually usablezfs clone tank/data@backup-2024-01-15 tank/data-verify# Run verification scripts on clone# Destroy when donezfs destroy tank/data-verify # 4. PARALLEL EXPERIMENTS# Multiple team members work on same base datazfs snapshot tank/dataset@basezfs clone tank/dataset@base tank/alice-experimentzfs clone tank/dataset@base tank/bob-experimentzfs clone tank/dataset@base tank/carol-experiment# Each clone shares base data, diverges independently # ============================================# PROMOTING CLONES# ============================================ # Sometimes a clone "wins" and should become the primary dataset# 'promote' reverses the parent-child relationship # Before promote:# tank/production (original)# tank/production@clone-source (snapshot)# tank/development (clone, depends on snapshot) zfs promote tank/development # After promote:# tank/development (now independent)# tank/development@clone-source (snapshot moved here)# tank/production (now a clone of development!) # The promoted clone is now the "primary" # and can live independently # ============================================# DESTROYING CLONES# ============================================ # Simple destroy (clone must have no dependents)zfs destroy tank/development # The origin snapshot cannot be destroyed while clones existzfs destroy tank/production@clone-source# Error: cannot destroy: snapshot has dependent clones # Options:# 1. Destroy the clone firstzfs destroy tank/developmentzfs destroy tank/production@clone-source # 2. Promote the clone, then destroy the old originzfs promote tank/development# Now tank/production depends on tank/development@clone-sourcezfs destroy tank/production # If no longer neededzfs destroy tank/development@clone-source # 3. Force destroy (destroys clones too - DANGEROUS)zfs destroy -R tank/production@clone-source# WARNING: This destroys ALL dependent clones!A clone depends on its origin snapshot—you cannot destroy the snapshot while clones exist. This creates a dependency chain. If you want the clone to become independent, use 'promote' to reverse the relationship. After promotion, the clone owns the shared blocks and the original dataset depends on them.
ZFS send and receive enable efficient snapshot replication—copying datasets between pools, systems, or sites. The mechanism is block-level and incremental, sending only changed blocks between snapshots.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
#!/bin/bash# ZFS Send/Receive Replication # ============================================# BASIC SEND/RECEIVE# ============================================ # Create a snapshot to sendzfs snapshot tank/data@tosend # Send snapshot to a file (backup to file)zfs send tank/data@tosend > /backup/data-snapshot.zfs # Receive snapshot from filezfs receive backup/data < /backup/data-snapshot.zfs # Pipe directly between pools (same machine)zfs send tank/data@tosend | zfs receive backup/data # Send to remote machine via SSHzfs send tank/data@tosend | ssh user@remote zfs receive backup/data # ============================================# INCREMENTAL SENDS (Essential for efficiency)# ============================================ # Initial full sendzfs snapshot tank/data@snap1zfs send tank/data@snap1 | ssh remote zfs receive backup/data # Later: create new snapshotzfs snapshot tank/data@snap2 # Incremental send: only changes between snap1 and snap2zfs send -i tank/data@snap1 tank/data@snap2 | \ ssh remote zfs receive backup/data # This sends ONLY the blocks that changed between snap1 and snap2# Massive bandwidth savings for minor changes # Incremental from any ancestor (more flexible)zfs send -I tank/data@snap1 tank/data@snap5 | \ ssh remote zfs receive backup/data# Sends snap2, snap3, snap4, snap5 as a stream # ============================================# REPLICATION STREAM (Best for full replication)# ============================================ # Full replication stream with all metadatazfs send -R tank/data@current | ssh remote zfs receive backup/data# -R replicates:# - All snapshots up to @current# - All properties (compression, quotas, etc.)# - Recursive datasets # Incremental replicationzfs send -R -i tank/data@previous tank/data@current | \ ssh remote zfs receive backup/data # ============================================# PRACTICAL REPLICATION SCRIPT# ============================================ #!/bin/bash# Replicate dataset to remote with incremental sends SOURCE="tank/production"DEST="backup/production"REMOTE="backup-server" # Find the last common snapshotLAST_REMOTE=$(ssh $REMOTE zfs list -H -o name -t snapshot -r $DEST | \ tail -1 | cut -d@ -f2) # Create new snapshotNEW_SNAP=$(date +%Y-%m-%d_%H-%M-%S)zfs snapshot ${SOURCE}@${NEW_SNAP} if [ -z "$LAST_REMOTE" ]; then # No remote snapshots - full send echo "Performing full send..." zfs send -R ${SOURCE}@${NEW_SNAP} | \ ssh $REMOTE zfs receive -F $DESTelse # Incremental send echo "Incremental send from @$LAST_REMOTE to @$NEW_SNAP" zfs send -R -I ${SOURCE}@${LAST_REMOTE} ${SOURCE}@${NEW_SNAP} | \ ssh $REMOTE zfs receive -F $DESTfi # ============================================# SEND OPTIONS# ============================================ # -p : Include properties (compression, quota, etc.)# -R : Replication stream (recursive + all snapshots + properties)# -I : Incremental from common ancestor (includes intermediates)# -i : Incremental (single snapshot difference)# -n : Dry run (estimate size without sending)# -v : Verbose (show progress)# -c : Use compressed WRITE representation (faster if both support)# -w : Raw send (preserves encryption, compressed blocks) # Estimate send size before doing itzfs send -nv tank/data@snap1zfs send -nv -i tank/data@snap1 tank/data@snap2 # ============================================# RECEIVE OPTIONS# ============================================ # -F : Force rollback if dataset has been modified# -s : Save partially received state (resumable)# -u : Don't mount after receiving# -d : Discard first element of path (tank/data → data)# -e : Discard all but last element (tank/data → data) # Resume interrupted receivezfs send -t <receive_resume_token> | zfs receive backup/data # Get resume token from partially received datasetzfs get receive_resume_token backup/dataZFS send produces a byte stream that compresses well. When sending over network, use compression: 'zfs send tank@snap | zstd | ssh remote "zstd -d | zfs receive pool"'. Or use SSH's built-in compression (-C flag). For local sends, 'zfs send -c' can send already-compressed blocks as-is.
Manual snapshot management doesn't scale. Production environments need automated creation and retention policies. Several community tools provide this automation.
| Tool | Description | Best For |
|---|---|---|
| zfs-auto-snapshot | Simple cron-based automation, ships with many distros | Basic setups, quick deployment |
| Sanoid/Syncoid | Advanced policy engine + sync tool, ZFS-aware | Complex retention policies, replication |
| zrepl | Daemon-based replication with hold points | Continuous replication, enterprise setups |
| znapzend | Daemon with flexible retention, send/receive | Scheduled replication with retention |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
# /etc/sanoid/sanoid.conf# Sanoid configuration for automated snapshots and retention # ============================================# TEMPLATE DEFINITIONS# ============================================ [template_production] # Frequent snapshots for production data frequently = 0 # No sub-hourly snapshots hourly = 24 # Keep 24 hourly snapshots daily = 30 # Keep 30 daily snapshots weekly = 8 # Keep 8 weekly snapshots monthly = 12 # Keep 12 monthly snapshots yearly = 2 # Keep 2 yearly snapshots autosnap = yes # Automatically create snapshots autoprune = yes # Automatically delete old snapshots [template_backup] # Less frequent snapshots for backup targets hourly = 0 daily = 30 weekly = 12 monthly = 24 yearly = 5 autosnap = no # Don't auto-snapshot (receives from source) autoprune = yes [template_temp] # Minimal retention for temporary data hourly = 4 daily = 2 weekly = 0 monthly = 0 yearly = 0 autosnap = yes autoprune = yes # ============================================# DATASET CONFIGURATIONS# ============================================ [tank/production] use_template = production recursive = yes # Apply to all child datasets [tank/databases] use_template = production hourly = 48 # Override: more hourly for databases process_children_only = yes [tank/home] use_template = production [tank/scratch] use_template = temp [backup/production] use_template = backup # ============================================# SYNCOID COMMANDS (in script or cron)# ============================================ # Sync tank/production to remote backup# syncoid --recursive tank/production backup-server:backup/production # Sync with bandwidth limiting (useful over WAN)# syncoid --recursive --bwlimit=50m tank/production remote:backup # ============================================# CRON SETUP# ============================================ # /etc/cron.d/sanoid# Run sanoid every 15 minutes for snapshot management*/15 * * * * root /usr/sbin/sanoid --cron > /dev/null 2>&1 # Run syncoid hourly for replication 0 * * * * root /usr/sbin/syncoid --recursive \ tank/production backup-server:backup/productionWith hourly=24, daily=30, weekly=8, monthly=12, yearly=2, you maintain: 24 hours of granularity, 30 days of daily points, 2 months of weekly points, 1 year of monthly points, and 2 years of yearly points. This is approximately 78 snapshots per dataset—manageable overhead with tremendous recovery flexibility.
While snapshots are nearly free to create, large numbers of snapshots and high-churn workloads can impact performance. Understanding these effects helps design sustainable snapshot policies.
After successfully replicating a snapshot with 'zfs send', you can convert it to a bookmark with 'zfs bookmark tank@snap tank#snap'. Bookmarks consume nearly zero space but preserve the reference point for future incremental sends. Useful when you don't need to access the snapshot locally anymore.
We've explored ZFS's powerful snapshot and clone capabilities—perhaps the most user-visible benefit of Copy-on-Write architecture. Let's consolidate the key insights:
Module Complete:
You've now completed the ZFS module, covering the revolutionary architecture that integrates file system and volume management, the storage pool model, end-to-end checksums and self-healing, RAID-Z without the write hole, and snapshots and clones for data management.
ZFS represents a paradigm shift in how we think about storage—from hoping hardware doesn't fail to verifying every block, from planning capacity in advance to sharing pools dynamically, from backup windows to instant snapshots. These capabilities, once rare and expensive, are now available on commodity hardware across multiple operating systems.
You've mastered ZFS: its Copy-on-Write architecture, pooled storage model, checksum-based integrity, RAID-Z redundancy, and snapshot/clone capabilities. ZFS sets the standard for enterprise and mission-critical storage. The skills you've learned apply whether deploying ZFS on a home NAS or architecting petabyte-scale storage infrastructure.