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 historicalzpool clearerases 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
| Cause | What it looks like | First thing to check |
|---|---|---|
| Scrub schedule never configured | scan: none requested on a pool in production for months | Look for cron entries and systemd timers; nothing exists |
| Systemd timers shipped but never enabled | Timer units exist on disk but systemctl list-timers shows nothing for zfs-scrub | systemctl list-timers --all | grep zfs |
| Cron job lost or overridden | Debian/Ubuntu ships /etc/cron.d/zfsutils-linux, but it may have been removed, or the host was rebuilt without the package config | cat /etc/cron.d/zfsutils-linux |
| Pool renamed or recreated | Per-pool timer unit (zfs-scrub-monthly@oldname.timer) no longer matches any pool | Compare enabled timers against zpool list -H -o name |
| Scrub paused and never resumed | scan: line shows a paused scrub; pause state survives reboot and pool export | zpool status | grep -i pause |
| Scrub preempted by resilver repeatedly | Scan line shows resilver activity, scrub never completes | Check zpool status and zpool history for resilver events |
| Scrub started but never finishing | Same scrub “in progress” for weeks on a large or busy pool | Check 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_timeis an Oracle Solaris property. On OpenZFS you parse thescan:line, or readlast_scrubbed_txgwhere 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@.timerandzfs-scrub-monthly@.timerare 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]Establish the last completed scrub. Parse the
scan:line fromzpool status <pool>. If it saysnone requested, no scrub has ever run on this pool and integrity is completely unknown. Treat that as a finding in itself.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.
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.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 againstzpool list -H -o name.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.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. Checkzpool iostat -v 1for a vdev that lags its peers.Check the permanent error list.
zpool status -vlists 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 untilzpool clearand is your recovery checklist.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
Days since last completed scrub (parsed from scan:) | The only meaningful integrity-freshness metric | 30+ days on any production pool |
| Scrub result (errors, bytes repaired) | Whether corruption was found and whether redundancy saved it | Any repaired errors (ticket); any uncorrectable errors or non-empty permanent error list (page) |
| Scrub duration trend | A scrub that took 4 hours and now takes 12 on similar data volume signals degrading devices or growing fragmentation | Duration up 50%+ at similar used capacity |
| Schedule execution (timer last-run, cron logs) | Scrubs fail silently; the result only exists if the schedule fires | Timer inactive, cron entry missing, per-pool instance name mismatch |
| Per-vdev CKSUM counters | Non-zero pinpoints the device returning corrupt data | Any non-zero value; sustained growth means failing hardware |
| Scrub paused state | Pause survives reboot and export, easy to forget | Any 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 statusinto 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 statusscan 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.
Related guides
- ZFS monitoring checklist: the signals every production pool needs
- ZFS monitoring maturity model: from survival to expert
- How ZFS actually works in production: a mental model for operators
- ZFS pool DEGRADED: redundancy lost and one failure from data loss
- ZFS pool FAULTED: when the pool can no longer serve I/O
- ZFS pool I/O is currently suspended: a hung pool and blocked I/O
- ZFS READ and WRITE errors: transport-level device failures in zpool status
- ZFS device FAULTED - too many errors: a disk ejected from the pool
- ZFS device UNAVAIL or REMOVED: a disk that fell off the bus






