The pool looks healthy. zpool status shows every device ONLINE, and the READ, WRITE, and CKSUM columns are all zero. No scrub errors have ever been reported. So the data is safe, right?

Not necessarily. Zero checksum errors means zero errors detected, not zero corruption. ZFS only discovers silent corruption when it reads a block and verifies its checksum, and most blocks on a typical pool are read rarely or never. The scrub is the only mechanism that systematically reads every allocated block and checks it. A pool that has not scrubbed in six months has unknown integrity.

The failure mode that makes this dangerous: scrubs are usually scheduled by cron or systemd timers, and those schedules silently stop working. The timer gets disabled, the cron job is lost in a migration, the pool was renamed and the per-pool timer no longer matches, or a scrub was paused and never resumed. Because “no scrub errors” and “no scrub at all” look identical in the error columns, the first sign is often a disk failure months later, when the resilver surfaces corruption that regular scrubs would have caught and repaired while redundancy was still intact.

This article covers how to tell whether scrubs are actually running, why they stop, and how to alert on scrub execution rather than scrub results.

What this means

A scrub walks the allocated block tree of the pool, reads every block, and verifies it against the checksum stored in metadata. When verification fails and redundancy exists (mirror, RAIDZ), ZFS repairs the block from the good copy. Without redundancy, the error becomes permanent and the affected file lands in the permanent error list.

The scan: line in zpool status is the authoritative record of scrub activity. It has three states:

  • In progress: scan: scrub in progress since <timestamp> with bytes scanned, rate, percent done, and ETA.
  • Completed: scan: scrub repaired 0B in 5h4m with 0 errors on Sun Feb 4 17:09:01 2018.
  • Never run: scan: none requested.

If the completed timestamp is months old, or the line says none requested, your “0 errors” status is meaningless. Corruption sits undetected on blocks nobody has read. When a device finally fails, you discover it during the resilver, which is the worst possible time: the pool is already DEGRADED and on a RAIDZ1 or two-way mirror there is no redundant copy left to repair from.

Two things make this worse than it looks:

  • There is a known OpenZFS issue (#11545) where scrub-repaired checksum errors do not always increment the per-vdev CKSUM counter, so even the CKSUM column is not a complete record.
  • Error counters are cumulative since the last zpool clear, and may reset on pool export/import. A historical zpool clear erases the evidence.

The only trustworthy statement about pool integrity is: “a scrub completed on this date, with this result.” Everything else is inference.

Common causes

CauseWhat it looks likeFirst thing to check
Scrub schedule never configuredscan: none requested on a pool in production for monthsLook for cron entries and systemd timers; nothing exists
Systemd timers shipped but never enabledTimer units exist on disk but systemctl list-timers shows nothing for zfs-scrubsystemctl list-timers --all | grep zfs
Cron job lost or overriddenDebian/Ubuntu ships /etc/cron.d/zfsutils-linux, but it may have been removed, or the host was rebuilt without the package configcat /etc/cron.d/zfsutils-linux
Pool renamed or recreatedPer-pool timer unit (zfs-scrub-monthly@oldname.timer) no longer matches any poolCompare enabled timers against zpool list -H -o name
Scrub paused and never resumedscan: line shows a paused scrub; pause state survives reboot and pool exportzpool status | grep -i pause
Scrub preempted by resilver repeatedlyScan line shows resilver activity, scrub never completesCheck zpool status and zpool history for resilver events
Scrub started but never finishingSame scrub “in progress” for weeks on a large or busy poolCheck progress rate in the scan: line over time

Quick checks

All read-only and safe to run any time.

# 1. Current scan state for every pool: last scrub date, in-progress, or none
zpool status | grep -E "pool:|state:|scan:"

# 2. Full detail for one pool, including permanent errors at the bottom
zpool status -v tank

# 3. Confirm the property everyone reaches for does NOT exist on OpenZFS
zpool get last_scrub_time tank
# expected: bad property list: invalid property 'last_scrub_time'

# 4. On recent OpenZFS: TXG up to which the last scrub completed (0 = never)
zpool get last_scrubbed_txg tank

# 5. Are the systemd timers actually active?
systemctl list-timers --all | grep -i zfs

# 6. Is the Debian/Ubuntu cron job present?
cat /etc/cron.d/zfsutils-linux

# 7. History of scrub invocations, useful when the scan line is ambiguous
zpool history tank | grep -i scrub | tail -n 10

Notes on what you will see:

  • Check 3 fails on OpenZFS. last_scrub_time is an Oracle Solaris property. On OpenZFS you parse the scan: line, or read last_scrubbed_txg where available, which is a transaction group number, not a timestamp. Zero means no scrub has completed since the property became available.
  • Check 5 commonly returns nothing even on systems where the timer unit files are installed. The units zfs-scrub-weekly@.timer and zfs-scrub-monthly@.timer are shipped by OpenZFS packaging but are not active until enabled per pool. “Vendor preset” state on the template does not mean an instance for your pool is running; check the instantiated unit state.
  • The Debian/Ubuntu cron job scrubs all pools monthly (second Sunday of the month). That cadence exists by default on that packaging, but it is easy to lose in a host rebuild or containerization.

How to diagnose it

Work through these in order. The goal is to answer one question: when did a scrub last complete, and what is supposed to start the next one?

flowchart TD
    A[Read scan: line in zpool status] --> B{What does it say?}
    B -->|none requested| C[No scrub ever run: schedule missing]
    B -->|completed, recent| D[Scrub ran: verify the schedule exists for the next one]
    B -->|completed, 30+ days ago| E[Schedule broken or disabled]
    B -->|in progress for weeks| F[Scrub stalled or I/O starved]
    B -->|paused| G[Paused scrub: persists across reboots]
    C --> H[Check cron and systemd timers]
    E --> H
    G --> I[Resume with zpool scrub]
    F --> J[Check progress rate and pool I/O load]
  1. Establish the last completed scrub. Parse the scan: line from zpool status <pool>. If it says none requested, no scrub has ever run on this pool and integrity is completely unknown. Treat that as a finding in itself.

  2. Compute days since completion. Over 30 days on a production pool with redundancy is a ticket-level gap. The target for production pools is a completed scrub every 7-14 days.

  3. Find the mechanism that is supposed to schedule scrubs. Check systemctl list-timers --all | grep zfs, /etc/cron.d/zfsutils-linux, any site-specific cron or Ansible-managed jobs, and tools like sanoid if deployed. If you find nothing, that is the root cause: scrubs were never scheduled.

  4. Verify the schedule targets this pool. Per-pool systemd timers are instantiated with the pool name (zfs-scrub-monthly@tank.timer). If the pool was renamed, recreated, or migrated from another host, the enabled instance may point at a pool that no longer exists. Compare enabled timer instances against zpool list -H -o name.

  5. Check for a paused or preempted scrub. A paused scrub stays paused across reboots and pool export/import; resume it with zpool scrub <pool>. Scrubs and resilvers are mutually exclusive per pool, and a resilver preempts a running scrub, so a pool with recurring device problems may never complete one. zpool history <pool> | grep -iE 'scrub|resilver' shows the sequence.

  6. If a scrub is in progress but never finishing, sample the scan: line twice, an hour apart, and compare bytes scanned. A scrub crawling for weeks on a live pool usually means heavy production I/O plus default throttling, or a slow device. Check zpool iostat -v 1 for a vdev that lags its peers.

  7. Check the permanent error list. zpool status -v lists files and objects with uncorrectable damage at the bottom. If this is non-empty, data loss has already occurred and the question shifts from “run a scrub” to damage assessment and restore. This list persists until zpool clear and is your recovery checklist.

Metrics and signals to monitor

SignalWhy it mattersWarning sign
Days since last completed scrub (parsed from scan:)The only meaningful integrity-freshness metric30+ days on any production pool
Scrub result (errors, bytes repaired)Whether corruption was found and whether redundancy saved itAny repaired errors (ticket); any uncorrectable errors or non-empty permanent error list (page)
Scrub duration trendA scrub that took 4 hours and now takes 12 on similar data volume signals degrading devices or growing fragmentationDuration up 50%+ at similar used capacity
Schedule execution (timer last-run, cron logs)Scrubs fail silently; the result only exists if the schedule firesTimer inactive, cron entry missing, per-pool instance name mismatch
Per-vdev CKSUM countersNon-zero pinpoints the device returning corrupt dataAny non-zero value; sustained growth means failing hardware
Scrub paused statePause survives reboot and export, easy to forgetAny scrub in paused state

One parsing gotcha for monitoring scripts: the scan: line format is not stable. When a scrub runs longer than a day, the duration field gains a “X days” token, which breaks naive awk parsers that assume fixed column positions. Match on keywords (scrub repaired, with N errors on) rather than column indexes, and test your parser against both a completed-scrub line and none requested.

Fixes

No schedule exists: create one

Pick one mechanism, not both, or you will get double scrubs.

On systemd-based systems, enable the shipped per-pool timer explicitly:

# Enable and start the monthly scrub timer for pool "tank"
systemctl enable --now zfs-scrub-monthly@tank.timer

# Verify it is actually scheduled
systemctl list-timers zfs-scrub-monthly@tank.timer

Do this for every pool. The timer is a template unit; enabling the template does nothing for pools you do not instantiate.

On Debian/Ubuntu, confirm /etc/cron.d/zfsutils-linux exists and survived any host rebuilds. If your provisioning system manages the host, put the cron file or the timer enablement into configuration management so the next rebuild does not silently drop it.

Scrub overdue right now: run one manually

# Start a scrub immediately
zpool scrub tank

# Watch progress
zpool status tank | grep -A2 scan:

A scrub is I/O intensive and competes with production traffic; on a large pool it can run for many hours or days under default throttling. Prefer starting it in a low-load window. If you need to interrupt it, zpool scrub -s <pool> stops it and zpool scrub -p <pool> pauses it, but a paused scrub survives reboots and an aborted scrub provides no integrity guarantee for the data it did not reach.

Expect the first scrub after a long gap to find errors. That is the point. If it reports correctable errors, the redundancy did its job and you now have a hardware investigation: check the per-vdev CKSUM counters and SMART data on the implicated device. If it reports uncorrectable errors, zpool status -v lists the damaged files and you are in restore-from-backup territory.

Paused or preempted scrubs

Resume a paused scrub with zpool scrub <pool>. If scrubs keep getting preempted by resilvers, fix the underlying device problem first; a pool that resilvers monthly has a hardware or cabling issue, and the scrub gap is a symptom.

Prevention

  • Alert on scrub freshness, not scrub results. Page on uncorrectable errors and non-empty permanent error lists. Ticket on any completed scrub with repaired errors, and on any pool where no scrub has completed in 30+ days. Most teams only alert on errors, which is exactly the gap that lets “no scrub ever ran” pass unnoticed.
  • Monitor that the schedule executes. Track the timer’s last-run timestamp or the cron job’s log output. A schedule that exists but never fires produces the same silent outcome as no schedule.
  • Manage scheduling in configuration management. Timers enabled by hand on a Friday afternoon do not survive the next host rebuild.
  • Keep scrub cadence at 7-14 days for production pools, scheduled in low-load windows. Monthly (the Debian cron default) is a floor, not a target.
  • Trend scrub duration. A lengthening scrub is an early indicator of device degradation and fragmentation, visible months before anything faults.
  • Export zpool status into time series. Point-in-time checks miss transitions, and error counters can be cleared. Continuous collection gives you the “days since scrub” metric for free.

How Netdata helps

  • ZFS pool state collection: Netdata’s ZFS collector charts pool health states and error counters over time, so pool transitions and error-counter growth become time-series data instead of a command someone has to remember to run.
  • Scrub state visibility: where the collector exposes zpool status scan data, scrub completion timestamps, errors found, and bytes repaired are retained historically, which enables alerting on scrub age (30+ days without a completed scrub) rather than only on scrub errors.
  • Per-device error counters: READ, WRITE, and CKSUM counts are charted over time, so a slowly dying disk shows up as counter growth between scrubs, not as a surprise during a resilver.
  • Correlation with pool health: scrub findings and error counters can be viewed alongside pool state (ONLINE/DEGRADED), capacity, and I/O latency on the same dashboard, which makes it obvious when errors coincide with a degrading device.