Runbook — T9 Incus btrfs pool: the full-qgroup stall and its fix

Closed 2026-10-21 by switching the pool from full btrfs qgroups to simple quotas (squota). This page is the postmortem, the rules that keep the pool healthy, and the runbook for recognizing the same failure again. The T9 hardware findings at the end are still open.

The system

Item Value
Disk Samsung T9 4 TB USB SSD (04e8:61fd, uas), /dev/sda
Pool partition /dev/sda2, btrfs, UUID aa55c400-b8fc-4998-8a64-fd876ab92c83
Mounts ~/t9-pool (fstab, noatime,nofail) and /var/lib/incus/storage-pools/t9-btrfs (Incus)
Other partition /dev/sda1, ext4, ~/t9; ~/workspace is a bind of ~/t9/workspace
Incus 6.15, incus.service runs ~/.guix-profile/bin/incusd --group incus-admin
Pool t9-btrfs: 30 instances (prod orchgraph HA cluster, test and dev fleets, pi-workers), 3 images
Size-capped profiles claude-test-profile (projects default, orchestrata) and pi-workers/default: root size=20GB

Symptoms (2026-10-19 → 2026-10-21)

Root cause

Full qgroups were enabled on the pool. Full qgroup accounting has to resolve every shared extent's backrefs when a subvolume is dropped. The test fleet deletes clones that share 23–31 GiB each with their base, so each drop made btrfs-cleaner walk backrefs for hours. Transaction commits queued behind it, which is what every sync, fsync and journal commit on the pool waited for.

At diagnosis: 21 deleted subvolumes were still queued for cleaning, and metadata was 19.8 GiB for 80.7 GiB of data.

Why quotas could not simply be turned off

Incus's btrfs driver re-enables quotas on its own (read from the Incus source, SetVolumeQuota):

The three profiles above set root size=20GB, so the next capped instance would have put full quotas straight back. Quotas had to stay on in a mode whose cost does not grow with sharing.

The fix (2026-10-21)

Simple quotas (kernel 6.7+, btrfs quota enable --simple) account each extent to the subvolume that allocated it, at allocation time. Dropping a subvolume does no backref walk.

It was rehearsed first on a loop-mounted btrfs image:

Mohamed ran the three sudo commands; Claude Code's auto-mode classifier blocks it from running btrfs quota on the pool.

  1. Disable full qgroups. sudo btrfs quota disable ~/t9-pool hung. The cleaner holds cleaner_mutex for the whole drop of one subvolume, and the drop in progress (subvolume 1048) was estimated at 20–30 hours.

  2. Unblock the cleaner. Setting the subtree-drop threshold to 0 tells the kernel to skip qgroup accounting on subtree drops:

    echo 0 | sudo tee /sys/fs/btrfs/aa55c400-b8fc-4998-8a64-fd876ab92c83/qgroups/drop_subtree_threshold
    

    The disable then completed, and all 21 queued deletions drained in about 15 s.

  3. Enable simple quotas:

    sudo btrfs quota enable --simple ~/t9-pool
    

    This sets the SIMPLE_QUOTA incompat feature; sysfs then shows qgroups/mode = squota.

Results

Measure Before After
sync on the pool (Incus stopped) 36–62 s 4–5 ms
Sync-write p50 on the same disk, same benchmark — btrfs 1.45 ms, ext4 1.24 ms
Queued subvolume deletions 21 0
Scrub (118 GiB) — no errors
Device error counters (btrfs device stats) — all 0
Sequential throughput — 1015 MiB/s read, 788 MiB/s write (the 10 Gbps link ceiling)

Incus then started in about 1 s. The prod orchgraph HA cluster came back with orchgraph-data-2 as main and orchgraph-data-1 as a sync replica, 0 behind.

State on 2026-10-22

What simple quotas do NOT give you

Rules

Recognizing it again

cat /sys/fs/btrfs/aa55c400-b8fc-4998-8a64-fd876ab92c83/qgroups/mode   # must be: squota
sudo btrfs subvolume list -d ~/t9-pool | wc -l                        # queued deletions; should drain fast
time sync                                                              # milliseconds, not seconds
ps -eo pid,stat,comm | awk '$2 ~ /D/'                                  # btrfs-cleaner / btrfs-transaction stuck in D?

T9 hardware findings (open)

Finding Evidence Fix
Link at half speed 10 Gbps on 1 lane (speed=10000, rx_lanes=1); the port supports 20 Gbps on 2 lanes Physical: try another USB-C cable rated for 20 Gbps, then another port
No TRIM provisioning_mode=full, discard_max_bytes=0. The bridge reports UNMAP support (VPD B2 LBPU=1; max 134,217,728 blocks, 64 GiB, per command) but does not set LBPME in READ CAPACITY(16), so the kernel never enables discard A udev rule setting provisioning_mode=unmap for 04e8:61fd (the documented fix for USB SSDs that hide UNMAP). Not applied: it changes how a disk holding prod data and the workspace handles discards, so it needs a backup check and a trial first. Mohamed decides.
Overheated in the past SMART: 431 min above the warning temperature and 35 min above critical; 31 °C at idle now Airflow; check Warning/Critical Comp. Temperature Time after heavy test runs
Unsafe shutdowns 103 of 108 power cycles The shutdown sync timeouts were the qgroup stall; confirm the count stops climbing after the next clean shutdown
One media/data-integrity error SMART Media and Data Integrity Errors: 1; 0% used, 100% spare Watch the counter: sudo smartctl -d sntasmedia -A /dev/sda

Maintenance

Open items

Related: RUL-038 (its 2026-10-19 evidence is this stall), Incus containers.