Quick answer

Sanoid keeps 24 hourly, 30 daily, and 12 monthly snapshots with this production template. These are retention counts, not intervals; frequently = 0 disables sub-hourly snapshots. Syncoid replicates over SSH using an explicit key. This documentation-derived guide includes scheduling, space-accounting commands, and a restore test to run on your backup host.

By LK Wood IV · 2026-06-02 · ~10 min read · St. Louis County, MO

Architecture diagram: a sanoid.timer systemd unit runs Sanoid every 15 minutes, which takes the hourly, daily and monthly snapshots of ZFS datasets (datapool/documents, vm-disks, media) on a Proxmox VE 8 source host using the production, vm_disks and media retention templates, while sanoid --monitor-snapshots pushes health to Uptime Kuma; syncoid then sends incremental zfs snapshots over SSH each day at 03:00 to a remote backuppool, where a restore test clones and mounts a snapshot to verify it.

ZFS snapshots are instant, zero-cost at creation time (they only consume space as data changes), and a critical part of a storage strategy. But ZFS doesn’t automate them — you need a policy engine. Sanoid is that engine. Syncoid adds replication to a second ZFS pool, either locally or over SSH.

This guide covers both: Sanoid for snapshot automation with retention policies, Syncoid for encrypted replication.

What this guide is based on

This procedure is documentation-derived. It includes no replication benchmark. I checked the Sanoid/Syncoid README, its sample configuration, and the OpenZFS manuals linked below for this guide. I ran no replication of my own and did not execute the installation, timers, monitoring integration, or restore commands on ZFS hosts. The output examples follow documentation templates. They are not captured terminal results. Run the verification and restore steps on your own hosts before relying on the backup.

Prerequisites

  • A ZFS pool already set up — see the ZFS on Proxmox guide if you’re starting from scratch
  • Debian 12 / Proxmox VE 8 host with ZFS set up as the Proxmox wiki describes (the commands apply to any Debian-based system with ZFS)
  • For Syncoid replication: SSH access to a destination host with a ZFS pool

Step 1: Install Sanoid

Sanoid is in the Debian 12 repositories:

apt update && apt install -y sanoid

On Proxmox VE (Debian-based), the same package applies. The Debian 12 package, sanoid 2.1.0-1.1, installs:

  • /usr/sbin/sanoid — the snapshot management daemon, plus /usr/sbin/findoid for finding file versions inside snapshots
  • /usr/sbin/syncoid — the replication tool (separate binary, same package)
  • /usr/share/doc/sanoid/examples/sanoid.conf — the example policy config, and /usr/share/sanoid/sanoid.defaults.conf — the defaults every template inherits (frequent_period = 15 lives there)
  • sanoid.timer and sanoid.service (the timer runs Sanoid every 15 minutes: OnCalendar=*:0/15), sanoid-prune.service, and /etc/cron.d/sanoid for hosts without systemd

The package does not create /etc/sanoid/. Sanoid, its timer and its cron job all read /etc/sanoid/sanoid.conf, so create it from the example first:

mkdir -p /etc/sanoid
cp /usr/share/doc/sanoid/examples/sanoid.conf /etc/sanoid/sanoid.conf

Step 2: Configure snapshot policies

Edit the config file:

nano /etc/sanoid/sanoid.conf

Sanoid uses templates to define retention policies. Apply them to datasets:

# /etc/sanoid/sanoid.conf
# Comments sit on their own lines on purpose: Sanoid reads this file with
# Config::IniFiles, which keeps a trailing "# ..." as part of the value.

# --- Templates ---
# Define retention policies once, reuse across datasets.
# Every number below is a retention COUNT: "keep the N most recent snapshots
# of this type", not "take one every N hours". Sanoid prunes down to the count.
# These are this guide's values. The sample shipped with the package keeps
# 36 hourly, 30 daily, 4 weekly and 3 monthly under the same template name.

[template_production]
# no sub-hourly snapshots
  frequently = 0
# keep 24 hourly snapshots (the last day)
  hourly = 24
# keep 30 daily snapshots (the last month)
  daily = 30
# no separate weekly retention
  weekly = 0
# keep 12 monthly snapshots (the last year)
  monthly = 12
  yearly = 0
# Sanoid runs every 15 minutes and takes a snapshot of a type once that
# type's period has passed since the last one
  autosnap = yes
  autoprune = yes

[template_media]
# media changes rarely: daily plus monthly is enough
  frequently = 0
  hourly = 0
  daily = 7
  weekly = 0
  monthly = 3
  yearly = 0
  autosnap = yes
  autoprune = yes

[template_vm_disks]
# frequently changing VM data: more hourly retention
  frequently = 0
# the last 48 hours at hourly granularity
  hourly = 48
  daily = 14
  weekly = 0
  monthly = 3
  yearly = 0
  autosnap = yes
  autoprune = yes

# --- Datasets ---
# Apply templates to your ZFS datasets

[datapool/vm-disks]
  use_template = vm_disks

[datapool/media]
  use_template = media

[datapool/backups]
# do not snapshot the PBS backup datastore; PBS handles its own versioning
  autosnap = no
  autoprune = no

[datapool/documents]
  use_template = production

Replace datapool with your pool name. List your pools and datasets with:

zfs list -r

To cover a dataset and all its children, add recursive = yes. To snapshot only the children (not the parent dataset itself), add process_children_only = yes. This is the clean way to cover a nested tree like datapool/vm-disks/vm-100, datapool/vm-disks/vm-101 without listing each one:

[datapool/vm-disks]
  use_template = vm_disks
# applies the vm_disks policy to every child dataset
  recursive = yes

Each value (hourly = 24, daily = 30, monthly = 12) is a retention count — the number of snapshots of that type Sanoid keeps before pruning the oldest. It is not an interval. Sanoid takes one snapshot of each enabled type per period (one per hour for hourly, one per day for daily) and prunes the type down to its configured count on every run when autoprune = yes. The frequently key adds sub-hourly snapshots at the interval set by frequent_period (15 minutes in sanoid.defaults.conf); it is set to 0 here because hourly granularity is enough for most homelab datasets.

Step 3: Enable and test the systemd timer

Sanoid ships with a systemd service and timer. Debian’s packaging enables the timer on install (its debian/rules runs the default dh sequence, which enables and starts shipped units); confirm on your host rather than assuming it:

# Check the timer's state, then make sure it is enabled and running
systemctl is-enabled sanoid.timer
systemctl enable --now sanoid.timer

# Verify the timer is active
systemctl status sanoid.timer

# Manually trigger a run to test
systemctl start sanoid.service

# Check output
journalctl -u sanoid.service -n 50

After a successful run, verify snapshots were created:

zfs list -t snapshot -r datapool/documents

You should see snapshots named like datapool/documents@autosnap_2026-05-03_14:00:00_hourly — the autosnap_ prefix is Sanoid’s own naming, as shown in the project README (checked 2026-08-22).

Step 4: Monitor snapshot health

Sanoid includes a monitoring command that outputs Nagios-compatible status:

sanoid --monitor-snapshots

This checks whether recent snapshots exist and whether the latest snapshot is within the expected age. The output is:

OK: all monitored datasets (datapool/documents, datapool/vm-disks) have fresh snapshots

Or, if a dataset has missed snapshots:

CRIT: datapool/documents newest daily snapshot is <age> old (should be < <limit>)

(Both lines follow the message templates in the sanoid script itself, checked 2026-08-22; a dataset with no snapshots of a type at all reports CRIT: <dataset> has no <type> snapshots at all!.)

For Uptime Kuma push monitoring:

Add to a cron job or systemd override:

#!/bin/bash
# /usr/local/bin/sanoid-health-check.sh
STATUS=$(sanoid --monitor-snapshots 2>&1)
if echo "$STATUS" | grep -q "^OK"; then
  curl -s "https://uptime.yourdomain.com/api/push/YOUR_PUSH_KEY?status=up&msg=Sanoid+OK"
else
  curl -s "https://uptime.yourdomain.com/api/push/YOUR_PUSH_KEY?status=down&msg=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.stdin.read().strip()))' <<< "$STATUS")"
fi
chmod +x /usr/local/bin/sanoid-health-check.sh
echo "*/30 * * * * root /usr/local/bin/sanoid-health-check.sh" >> /etc/crontab

Step 5: Syncoid replication

Syncoid replicates datasets from a source ZFS pool to a destination ZFS pool. The destination can be:

  • A second pool on the same host (local backup copy)
  • A remote host over SSH
  • A Proxmox Backup Server node with a ZFS datastore

Set up SSH key authentication to the destination:

# On the source host, generate a key for the root user (or a dedicated sanoid user)
ssh-keygen -t ed25519 -f /root/.ssh/sanoid_id -N ""

# Copy to the destination host
ssh-copy-id -i /root/.ssh/sanoid_id.pub root@backup-host

Test a manual replication:

# Replicate datapool/documents to backuppool/documents on a remote host
syncoid --recursive \
  --sshkey /root/.ssh/sanoid_id \
  datapool/documents \
  root@backup-host:backuppool/documents

The --sshkey flag points Syncoid at the key you generated above (--sshkey=FILE in syncoid --help); without it, Syncoid uses root’s default SSH identity and the sanoid_id key is never used. The --recursive flag replicates all child datasets. Syncoid automatically finds the most recent common snapshot between source and destination and sends only the incremental delta.

The example above is a push: it runs on the source and sends to a remote destination (localdataset user@host:remotedataset). You can also pull, which runs on the backup host and reads from the remote source (user@host:remotedataset localdataset). Pull is the safer topology for a dedicated backup server when the source has no credentials granting access to the backup host. The backup box holds the replication key. A source compromise then does not expose an SSH key that can delete the backups.

# PULL: run this ON the backup host. It reads datapool/documents from the
# production host and writes it to backuppool/documents locally.
syncoid --recursive \
  root@prod-host:datapool/documents \
  backuppool/documents

Syncoid resolves the incremental automatically in either direction. The direction changes which machine initiates the SSH connection and holds the key.

The first run sends the entire dataset. Large pools take time. Run it manually first to verify the connection and transfer rate before scheduling.

Schedule Syncoid with systemd:

Create a systemd service:

# /etc/systemd/system/syncoid-replicate.service
[Unit]
Description=Syncoid ZFS Replication
Requires=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/sbin/syncoid --recursive --sshkey /root/.ssh/sanoid_id datapool/documents root@backup-host:backuppool/documents
ExecStartPost=/usr/sbin/syncoid --recursive --sshkey /root/.ssh/sanoid_id datapool/vm-disks root@backup-host:backuppool/vm-disks

Create the timer:

# /etc/systemd/system/syncoid-replicate.timer
[Unit]
Description=Run Syncoid replication daily

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

Enable:

systemctl daemon-reload
systemctl enable --now syncoid-replicate.timer

Step 6: Verify replicated snapshots

On the destination host, confirm snapshots arrived:

# On backup-host
zfs list -t snapshot -r backuppool/documents | tail -10

You should see snapshots with Sanoid’s naming convention, matching what’s on the source.

Test a restore from a replicated snapshot:

# On backup-host: clone a snapshot to a test dataset
zfs clone backuppool/documents@autosnap_2026-05-03_00:00:00_daily backuppool/restore-test

# Mount it and verify content
zfs set mountpoint=/mnt/restore-test backuppool/restore-test

# Check files
ls /mnt/restore-test

# Clean up
zfs destroy backuppool/restore-test

This step is not optional. Snapshots that can’t be mounted and read are not useful backups. Run restore tests when you first set this up and periodically afterward.

Retention math

Snapshot count is not a space estimate. The OpenZFS property manual distinguishes three useful measurements:

  • usedbysnapshots (USEDSNAP) measures space retained by this dataset’s snapshots collectively, which removing all those snapshots would free.
  • A snapshot’s USED measures blocks unique to that snapshot. Summing those values misses blocks shared between snapshots.
  • written measures space referenced by a dataset that was written since its previous snapshot. written@SNAPSHOT selects an earlier comparison point. Neither is a total snapshot-retention bill.

Read the pool’s accounting instead of multiplying counts. The zfs-list manual defines -o space as a shortcut for the space columns, including USEDSNAP:

zfs list -o space -r datapool/documents
zfs list -t snapshot -o name,used,refer -r datapool/documents
zfs get written datapool/documents

Run these inspection commands on your own pool; this guide reports no measured space results of its own. With child datasets, inspect each row’s USEDSNAP; the parent’s children are accounted for separately under USEDCHILD.

Worked example (hypothetical): start with 100 GB of live data and take a snapshot. Over the next 30 days, overwrite a different 1 GB of original blocks each day, exactly once. Keep the starting snapshot throughout. Assume equal-sized replacement blocks, no compression or deduplication, no clones, and ignore metadata overhead.

The live data remains 100 GB. The starting snapshot pins 30 GB of replaced blocks: 1 GB/day × 30 days = 30 GB. Live data plus retained old data is therefore 130 GB in this simplified model. Hourly and daily snapshots during this period reference those same block versions; they do not each add another gigabyte. Rewriting the same blocks repeatedly between retained snapshots changes the result. ZFS compression applies to snapshot data as well as live data; if your data compresses at 1.5:1, snapshot overhead compresses proportionally. Measure your workload before sizing a year of retention.


Sanoid snapshots protect against accidental deletion and filesystem corruption within a single pool — but a snapshot on the same pool is not one of your 3-2-1 backup copies; it dies with the pool. For off-machine protection (fire, hardware failure), combine with Syncoid replication to a second physical host or restic to Backblaze B2. The ZFS on Proxmox setup guide covers the pool creation that Sanoid manages.

Sources

Frequently asked questions

What is Sanoid and why do I need it for ZFS?
ZFS can take snapshots natively with ‘zfs snapshot’, but it provides no built-in automation or retention policy enforcement. Sanoid is a policy daemon that runs on a schedule, creates snapshots according to a retention config, and prunes old snapshots automatically. Without Sanoid (or an equivalent tool), you either take no automated snapshots or write and maintain your own cron scripts.
How is Sanoid different from Proxmox Backup Server for ZFS?
Proxmox Backup Server backs up VM and LXC content — it copies the data out of the VM/LXC into PBS’s chunk-based format. Sanoid snapshots operate at the ZFS dataset level and capture the entire filesystem state including the OS inside a VM, not just the backup data. Sanoid is especially useful for datasets that PBS doesn’t see directly — ZFS datasets used for file storage, Docker volumes, or datasets on a TrueNAS host outside Proxmox. The two tools are complementary, not competing.
What does Syncoid do that ‘zfs send | zfs recv’ doesn’t?
Syncoid is a wrapper around zfs send/recv that handles incremental sends (it finds the common snapshot between source and destination and only sends the delta), SSH transport with compression, and mbuffer buffering to prevent I/O stalls. Raw ‘zfs send | zfs recv’ requires you to track the incremental send token manually. Syncoid does this automatically, making replication a single command.
Can I replicate to a non-ZFS destination?
ZFS replication (via Syncoid or raw zfs send) requires a ZFS pool on the destination. For non-ZFS destinations (Backblaze B2, an ext4 NAS, a cloud VM), use restic instead — restic deduplicates and encrypts at the block level and writes to any storage backend. For replicating ZFS to another ZFS machine (a second NAS, a PBS server with a ZFS pool), Syncoid is the right tool.
How often should ZFS snapshots run?
This guide’s production template keeps 24 hourly, 30 daily, and 12 monthly snapshots. With frequently = 0, its finest granularity is hourly, not sub-hourly. Once the history has accumulated, uninterrupted scheduling provides roughly a day of hourly recovery points, a month of daily points, and a year of monthly points. A timer check every 15 minutes does not create a snapshot on every check. Adjust the policy to how much recent work you can afford to lose.

Evidence ledger

Last updated
Methodology
See our methodology for research and review standards.
Update log
  • 2026-09-05 — Page updated.
Corrections
Spotted an error or a stale number? Email hello@techfuelhq.com. Confirmed corrections are added to the update log above.

About the author

Written by Lowell K. Wood IV, who builds and runs TechFuelHQ from St. Louis, Missouri.