<-- All Articles

FreeBSD 15.1-RELEASE shipped on 16 June 2026 carrying OpenZFS 2.4.2. If you were hoping for a landmark ZFS release, this is not one. There is no new on-disk feature flag you have to reason about before enabling, no change to how vdevs behave, and nothing here that invalidates a configuration that already works. What you get is a set of scan-scheduling and lock-contention improvements plus one small CLI convenience.

This piece is deliberately a companion to ZFS Tuning for Production FreeBSD Servers, not a replacement for it. Everything in that article -- ARC sizing, recordsize per workload, compression choice, when L2ARC and SLOG actually earn their slot -- carries forward to 15.1 unchanged. Read that one for the baseline. Read this one to find out what to re-measure after the upgrade and what you can safely ignore.

First, confirm what you are actually running. The OpenZFS version is independent of the userland version string, and on a system upgraded in place it is easy to assume you got the new kernel module when you did not.

# Userland and kernel versions
freebsd-version -kru
# 15.1-RELEASE
# 15.1-RELEASE
# 15.1-RELEASE

# OpenZFS version as loaded
zfs version
# zfs-2.4.2-FreeBSD_g1a2b3c4d
# zfs-kmod-2.4.2-FreeBSD_g1a2b3c4d

If zfs and zfs-kmod disagree, you booted a new userland with an old module still loaded, or the reverse. Fix that before you measure anything. The full upgrade sequence, including the jail side, is in the 15.0 to 15.1 pkgbase upgrade runbook.

Scrub and Resilver Now Yield in Sub-Second Slices

This is the change most likely to show up in your graphs. ZFS scans -- scrub, resilver, and the obsolete-block cleanup after a device removal -- run as background work that is deliberately preempted so foreground I/O keeps its latency budget. The scheduler works in slices: the scan issues work for a minimum interval, then yields. Historically that minimum was measured in whole seconds. In OpenZFS 2.4.2 those minimums dropped to sub-second values.

The practical effect is finer interleaving. On a pool sitting mostly idle you will notice nothing, because the scan was already free to run flat out. On a pool under sustained production I/O, the old behaviour was a scan that grabbed the queue for a full slice, pushed application latency into a visible spike, released, and repeated. Shorter slices smooth that out. Upstream frames it as improved throughput under heavy load; the effect I would expect you to observe first is reduced p99 latency jitter on the application side during a scrub, with the scan itself making steadier forward progress instead of stalling whenever the workload gets busy.

The controlling tunables are still present and the names have not changed:

# Current scan scheduling values
sysctl vfs.zfs.scrub_min_time_ms \
       vfs.zfs.resilver_min_time_ms \
       vfs.zfs.obsolete_min_time_ms

# How aggressively scans are allowed to queue per vdev
sysctl vfs.zfs.vdev.scrub_min_active \
       vfs.zfs.vdev.scrub_max_active

# Watch a scrub in progress, refreshing every 10 seconds
zpool status -v -T d tank 10

Do not preemptively raise vfs.zfs.scrub_min_time_ms to claw back the old behaviour. If your scrubs were tuned by hand because they were disrupting production, the honest move on 15.1 is to reset those tunables to defaults and measure again. There is a reasonable chance the old hand-tuning is now working against you.

What to re-measure: total scrub wall time on your largest pool, and application latency during that window. If you have never recorded a baseline scrub duration, record one on 15.1 and keep it. Resilver windows are the number that actually matters for risk -- a shorter resilver is a shorter period of reduced redundancy. The FreeBSD Server Monitoring Guide covers wiring these into existing metrics rather than eyeballing them.

zfs_arc_evict_batches_limit and ARC Lock Contention

OpenZFS 2.4.2 adds a tunable that caps how many eviction batches the ARC processes in a single pass, reducing lock contention during eviction. On FreeBSD it surfaces as a sysctl in the ARC namespace. Confirm the exact name on your own box rather than trusting a copied command:

# Find it and read its description
sysctl -a | grep -i evict
sysctl -d vfs.zfs.arc.evict_batches_limit

ARC eviction contention is a narrow problem. It shows up on high core count machines with an ARC under constant pressure -- steady heavy read traffic against a working set larger than ARC, so eviction runs continuously instead of in occasional bursts. The symptom is CPU time that does not correspond to useful work: high system time, ARC hit rate unremarkable, disks nowhere near saturated.

# Is eviction actually the hot path?
sysctl kstat.zfs.misc.arcstats.evict_skip
sysctl kstat.zfs.misc.arcstats.memory_throttle_count
sysctl kstat.zfs.misc.arcstats.hits kstat.zfs.misc.arcstats.misses

# Where is system time going
top -SHz -o time

# Profile kernel stacks for 30 seconds
dtrace -n 'profile-997 /arg0/ { @[stack()] = count(); } tick-30s { exit(0); }'

If ARC eviction stacks do not dominate that profile, this tunable is not your problem and the default is fine. I would leave it alone on every machine I run unless a profile points directly at it. That is the honest answer for most readers: it is a fix for people who already know they have this specific problem.

zpool prefetch Learns BRT Metadata

zpool prefetch pulls a class of pool metadata into ARC ahead of time so the first operations needing it do not pay cold-read latency. In 2.4.2 it gained block reference table support, and the -t type argument became optional -- with no type given, all supported metadata types are prefetched.

# Previously: type was mandatory
zpool prefetch -t ddt tank

# 2.4.2: BRT is a valid type, and -t is optional
zpool prefetch -t brt tank
zpool prefetch tank

# Inspect BRT state and space savings
zdb -T tank

BRT is the table behind block cloning -- the copy_file_range(2) fast path that turns a copy within a dataset into a reference update instead of a data write. It is not deduplication. Dedup runs synchronously on every write against a large DDT that has to be sized carefully; block cloning only does work when something explicitly asks for a copy, and the BRT stays small. If you run workloads that clone heavily -- container image layers on ZFS, VM image provisioning, build systems that copy trees around -- warming BRT after a reboot is a cheap way to avoid a cold-start penalty on the first wave of clones. If you do not clone, this changes nothing for you.

bectl -E for Empty Boot Environments

bectl(8) in 15.1 gained -E, which creates an empty boot environment instead of cloning the active one. That matters for ZFS-on-root upgrade workflows where you want a clean target to install into rather than a snapshot of the current system.

# Traditional: clone the running BE before an upgrade
bectl create pre-15.1-upgrade

# New in 15.1: create an empty BE to install into
bectl create -E clean-install

bectl list -H
# default            NR  /  12.4G  2026-06-16 09:12
# pre-15.1-upgrade   -   -  1.2M   2026-07-26 08:03
# clean-install      -   -  8K     2026-07-26 08:05

For a routine freebsd-update or pkgbase upgrade, keep cloning. That is still the right pattern and it is what buys you the one-command rollback. -E is for the case where you are staging a fresh base system install into a new dataset and specifically do not want to inherit the current one's state.

Adjacent 15.1 Changes That Land on Storage Servers

Three fixes in 15.1 are not ZFS changes but tend to affect the same machines:

  • The NFS client now handles case-insensitive filesystems properly, which matters when exporting to or mounting from mixed environments.
  • NFSv4 diskless boot supports nfsuserd(8) configurations, closing a gap for diskless clients that need proper ID mapping.
  • iSCSI TargetName matching is case-insensitive again, per RFC 3722. If you worked around a target that stopped matching after an earlier upgrade by rewriting the IQN to match case exactly, that workaround is no longer required -- though it is also not harmful, so there is no urgency to undo it.

The broader set of 15.1 changes outside storage is covered in what changed in FreeBSD 15.1.

The Short Version

Upgrade for the scan scheduling. That is the change with a real operational payoff on busy pools, and it is the one thing worth graphing before and after. Reset any hand-tuned scrub or resilver timing back to defaults and re-measure instead of carrying old workarounds forward. Leave zfs_arc_evict_batches_limit at its default unless a kernel profile tells you otherwise. Use zpool prefetch with BRT only if you actually clone blocks. Everything else in your ZFS configuration -- ARC limits, recordsize, compression, vdev layout -- should come across from 15.0 untouched, and if it does not, that is a sign the original tuning was fragile rather than a sign that 15.1 broke something.

Tunable names and behaviour above are taken from the OpenZFS 2.4 man pages and the FreeBSD 15.1-RELEASE release notes. Sysctl names on FreeBSD occasionally differ from the upstream module parameter names, so verify with sysctl -d on your own system before scripting against them.

Running ZFS in production on FreeBSD and want a second opinion on pool layout or an upgrade plan? See our FreeBSD engineering services or schedule a consultation.

Need help with a ZFS upgrade or pool review?

We plan and run FreeBSD upgrades on production storage -- boot environments, scrub and resilver budgets, pool layout review. Let us handle the risky part.

Book a Free 30-Min Review