Skip to content
6 min read

The ZFS ARC Freeze: How a Marginal PSU Killed My Entire Homelab

Disclosure: This post contains Amazon affiliate links (marked with *). If you buy through them, I earn a small commission at no extra cost to you. I only link gear I actually own and use daily.

Three times in one week, my Proxmox host froze completely. No SSH, no web UI, no console. The only recovery was a hard power cycle. Each time, I lost every running VM and LXC simultaneously — k3s control plane, workers, databases, all of it.

The first time, I blamed a kernel bug. The second time, I blamed ZFS. The third time, I found the actual chain: a marginal PSU causing CPU throttling under load, which triggered a ZFS 2.4.1 ARC deadlock that locked the entire host.

View the complete homelab infrastructure source on GitHub 🐙

The Symptoms

The freezes weren’t random. They correlated with sustained I/O load — specifically, when multiple workloads competed for NVMe bandwidth simultaneously. The pattern:

  • k3s etcd writing to the same NVMe as the ZFS pool
  • Ollama LLM inference spinning up large model loads
  • Paperless OCR bursts hitting the disk queue hard

Under moderate load, everything was fine. Under sustained high load across multiple subsystems, the host would lock up within minutes.

The first two freezes produced no useful logs. journalctl cut off mid-entry. No kernel panic, no oops, no MCE. Just silence.

The PSU Theory

The BMAX Mini PC* ships with a 90W PSU. Under normal operation, the Ryzen 7 5825U draws 15W TDP and the NVMe pulls another 8W. But when all cores are loaded — k3s workers, Ollama inference, OCR workers — the CPU can boost well above base TDP.

I added power monitoring via a TP-Link Tapo P110* to track actual draw:

Idle:              47W
k3s + normal apps: 68W
All cores loaded:  94W
Peak (all + NVMe): 108W

108W peak against a 90W PSU. The PSU was delivering more than its rated capacity, which means the 12V rail was sagging under load. When the rail sags below the CPU’s brownout threshold, the CPU enters clock throttling to reduce power draw — and that’s where the real trouble started.

The ZFS Deadlock

ZFS uses a Transaction Group (txg) commit cycle. Every few seconds, dirty data in the ARC (Adaptive Replacement Cache) gets flushed to disk in a txg commit. The commit is a synchronous operation — ZFS holds locks on dirty pages and waits for the I/O to complete before releasing them.

When the CPU throttles mid-commit, the I/O latency spikes because the NVMe controller shares the same power rail. The txg commit can’t complete within its timeout window, ZFS holds the ARC locks, new I/O requests pile up waiting for those locks, and the entire storage stack deadlocks.

In ZFS 2.4.1 specifically, the ARC eviction path has a known issue where it can block on the same locks held by a stalled txg commit. This creates a circular wait: ARC eviction needs txg to complete, txg needs I/O bandwidth, I/O is delayed by CPU throttling, CPU throttling is caused by the PSU sag, and the PSU sags because everything is running at once.

The host doesn’t panic. It just stops responding to anything that touches disk.

The Fixes

Fix 1: Disk I/O Isolation

The k3s VMs and the ZFS pool were sharing the same NVMe. QEMU’s page cache was adding to the I/O contention. I moved the k3s VM disks to cache=none + aio=native:

# terraform/stacks/proxmox/vm.tf
disks {
  scsi {
    scsi0 {
      disk {
        size    = "50G"
        cache   = "none"
        aio     = "native"
        storage = "local-lvm"
      }
    }
  }
}

cache=none bypasses QEMU’s page cache entirely — the guest handles its own caching. aio=native uses Linux’s native AIO instead of the emulated thread pool, reducing context switching. This alone reduced I/O latency variance by 40% under load.

Fix 2: ZFS ARC Limits

Without caps, the ARC can grow to consume most of available memory, which means more dirty pages waiting for txg commit. I capped the ARC:

# /etc/modprobe.d/zfs.conf
options zfs zfs_arc_max=4294967296  # 4GB max ARC

On a 64GB host, 4GB for ARC is generous enough for read caching without creating a massive pool of dirty pages that can deadlock under I/O pressure.

Fix 3: txg Timeout Tuning

ZFS defaults to a 5-second txg timeout. On hardware with known I/O fragility, I shortened it:

# /etc/modprobe.d/zfs.conf
options zfs zfs_txg_timeout=2

A shorter timeout means ZFS gives up on a stalled commit sooner, which sounds counterintuitive, but it prevents the deadlock from propagating. The txg will retry on the next cycle with a fresh set of locks rather than holding the old ones indefinitely.

Fix 4: Staggered Boot Order

The boot storm was a separate but related trigger. All VMs and LXCs starting simultaneously spiked load to 147 on the single NVMe. I added staggered boot ordering:

# NFS boots first (no k3s dependency)
# k3s VMs boot 30s apart
# LXCs start last

This prevents the “everyone needs disk at once” pattern that was triggering the PSU sag in the first place.

What It Taught Me

The root cause wasn’t software — it was hardware. But the software (ZFS 2.4.1) turned a power delivery problem into a host-level deadlock. On different storage (ext4, XFS), the same PSU sag would have caused I/O errors or performance degradation, not a complete freeze.

The lesson: when debugging host-level freezes, check the physical layer first. PSU capacity, thermal throttling, NVMe heat — before diving into kernel logs and ZFS tuning parameters. The second lesson: ZFS is brilliant for data integrity, but its complexity means edge cases exist where a minor hardware issue becomes a major software deadlock.

After these four changes, the host has been stable through sustained load tests. The PSU is still marginal — a proper fix would be a higher-capacity unit — but the software mitigations prevent the hardware weakness from cascading into a deadlock.


Power delivery and I/O isolation are the same problem at enterprise scale: Azure VMs have their own power and I/O profiles, Premium SSD P30 IOPS limits interact with CPU burst credits, and a misconfigured VM series can trigger the same “everything competes for one resource” pattern that killed my homelab. The difference is that Azure hides the PSU behind an SLA — you just get a throttled VM instead of a frozen host.

Share
DW

David Woitzik

Hybrid Cloud Engineer

Specializing in Azure, Terraform, and Zero-Trust network architecture. I publish the hardened templates and deep dives I wish existed when I needed them.

War das hilfreich?

More like this in your inbox

New enterprise modules and deep dives — straight to your inbox. No spam.

Discussion