Unraid Memory / IO Pressure Runbook¶
When to use: Load average > 10×core count, sys CPU > 50%, NVMe %util near 100, OOM kills in dmesg, containers flipping unhealthy, or docker ps taking > 10 s. These symptoms together almost always indicate page cache thrashing from memory exhaustion, not a misbehaving user process.
Estimated time: 5–10 minutes for full triage + mitigation.
Signature: page cache thrashing (the common case)¶
A specific four-signal fingerprint distinguishes cache thrashing from a genuine disk-bound workload:
| Signal | Thrashing | Real disk workload |
|---|---|---|
%system CPU |
> 50% (kswapd + kworker) | < 20% |
vmstat bi (blocks in) |
> 100k/s | high, but matches user reads |
iowait % |
low (1–5%) — CPU is sys, not wait | high (> 20%) |
Page cache (free -h) |
< 25% of RAM | normal (> 40% of RAM) |
If all four match thrashing → root cause is memory pressure, not the disk. Adding swap or freeing RAM fixes it. Investigating disk IO is a red herring until memory is relieved.
Triage in 5 steps¶
Step 1 — Confirm signature (parallel commands)¶
ssh root@unraid 'echo "=== LOAD ==="; uptime
echo "=== VMSTAT ==="; vmstat 1 3
echo "=== FREE ==="; free -h
echo "=== IOSTAT ==="; iostat -xmN 2 2 | grep -E "^(avg-cpu|nvme0n1|loop0) \$|^(nvme0n1|loop0) "
echo "=== CGROUP IO ==="; cat /sys/fs/cgroup/io.stat'
Interpretation:
vmstatcolumns of interest:r(runnable),b(blocked, D-state),in(interrupts),cs(context switches),bi/bo(block in/out),us/sy/id/wa(CPU split).r > 4×coresandb > 5→ system is saturated.sys > 50%+wa < 5%→ CPU is burning in kernel (kswapd / kworker), not waiting on disk.cgroup io.statshows cumulativerbytes/riosper device.259:0isnvme0n1. Cumulative reads > 10× physical RAM after < 3 days uptime = cache miss amplification.
Step 2 — Confirm memory exhaustion¶
ssh root@unraid 'free -h; echo "=== SWAP ==="; swapon --show; echo "=== TOP MEM ==="; ps aux --sort=-%mem | head -15'
Server has 31 GiB RAM. If used > 28 GiB and swap = 0 → memory exhausted → page cache evicted → thrashing confirmed.
Step 3 — Find OOM kills (proves memory was the constraint)¶
OOM entries show: which process was killed, its total-vm vs anon-rss (real RAM), and oom_score_adj (was it deliberately prioritized for killing?).
Read carefully: total-vm is virtual address space, not RAM. Real RAM is anon-rss. Chromium reserves ~50 GB virtual per process; that's normal, not a leak. The OOM event itself is the signal.
Step 4 — Identify culprit(s)¶
ssh root@unraid 'ps aux --sort=-%mem | head -15
echo "=== DOCKER UNHEALTHY ==="
docker ps --filter health=unhealthy --format "{{.Names}} | {{.Status}}"
echo "=== FLOWCOLL/CHROMIUM/JVM ==="
ps aux | grep -iE "flowcoll|chromium|java" | grep -v grep | awk "{print \$2, \$4\"%\", \$6/1024\"MB\", \$11}" | head -20'
Common memory consumers on this server (2026-07 baseline):
| Process | Typical RSS | Notes |
|---|---|---|
flowcoll (elastiflow) |
3–6 GB | Watch for leak — grows back after OOM |
opensearch (JVM -Xmx2g) |
3 GB | Bounded by JVM heap |
emby / jellyfin |
0.5–1 GB | Stable |
~~chromium (synthnews Playwright)~~ |
~~50 MB RSS, ~50 GB virtual~~ | Removed 2026-07-18 — synthnews stack decommissioned |
| Stack of small containers | ~10 GB total | Death by 1000 cuts |
Step 5 — Mitigate (decision tree)¶
graph TD
Q1{"Q1: Page cache thrashing
signature present?"}
Q1 -->|No| DISK["Real disk-bound workload
Use iotop -baoP to find reader"]
Q1 -->|Yes| Q2{"Q2: Is swap configured?
(swapon --show)"}
Q2 -->|No| ADDSWAP["Add swap on vortex (procedure below)
Immediate relief"]
Q2 -->|Yes| Q3{"Q3: Is swap used > 75%?"}
Q3 -->|No| WARM["Memory pressure already relieved
Wait 5 min for cache to warm"]
Q3 -->|Yes| Q4{"Q4: Single process growing
monotonically?"}
Q4 -->|Yes| LEAK["Leak. Limit container:
docker update --memory=Ng --memory-swap=Ng <name>
Or restart the leaking service and monitor"]
Q4 -->|No| EXCEED["Workload genuinely exceeds RAM
Reboot or reduce running containers"]
Procedure: add swap on btrfs vortex¶
Swap on btrfs requires a NOCOW subvolume (CoW + swap = corruption). On this server, vortex is btrfs on nvme0n1p4.
ssh root@unraid 'set -e
# 1. Create NOCOW subvolume (one-time)
btrfs subvolume create /mnt/vortex/@swap
chattr +C /mnt/vortex/@swap
# 2. Create swapfile(s) — repeat per 16 GB chunk
truncate -s 0 /mnt/vortex/@swap/swapfile
chattr +C /mnt/vortex/@swap/swapfile # must be set BEFORE fallocate
fallocate -l 16G /mnt/vortex/@swap/swapfile
chmod 600 /mnt/vortex/@swap/swapfile
# 3. mkswap + swapon with priority 100
mkswap /mnt/vortex/@swap/swapfile
swapon -p 100 /mnt/vortex/@swap/swapfile
# 4. Verify
swapon --show
free -h'
For 32 GB total, create swapfile2 with the same steps.
Make persistent across reboots¶
Append to /boot/config/go (always back up first: cp /boot/config/go /boot/config/go.bak.$(date +%Y%m%d-%H%M%S)):
# Activate swap files on vortex btrfs (NOCOW subvolume @swap)
if [ -f /mnt/vortex/@swap/swapfile ]; then
swapon -p 100 /mnt/vortex/@swap/swapfile 2>/dev/null
fi
if [ -f /mnt/vortex/@swap/swapfile2 ]; then
swapon -p 100 /mnt/vortex/@swap/swapfile2 2>/dev/null
fi
The subvolume @swap and the swapfiles persist (they're files on btrfs); only the swapon calls need to run at boot.
Procedure: limit a leaking container¶
When a specific container repeatedly OOMs and grows back:
# Inspect current limits
ssh root@unraid "docker inspect <name> --format '{{.HostConfig.Memory}} {{.HostConfig.MemorySwap}}'"
# Apply hard limit (e.g., 4 GB)
ssh root@unraid "docker update --memory=4g --memory-swap=4g <name>"
# Restart to drop current bloat
ssh root@unraid "docker restart <name>"
To persist the limit, edit the stack TOML and redeploy via Komodo: add deploy.resources.limits.memory in the service definition.
Why this is NOT usually the disk¶
The IO on nvme0n1 during a thrashing event can hit 3.5 GB/s reads and 15k IOPS, looking exactly like a runaway reader. It isn't:
iotop -baoPshows only small reads scattered across many processes (docker health-check exec's).- The reads come from
loop0(/boot/bzmodulessquashfs) — the OS binaries/libraries that get re-read on everyexecbecause page cache is empty. - Cumulative NVMe reads can exceed 40 TB in 65 h of uptime — pure cache-miss amplification.
Fix the memory → cache recovers → IO drops 20–30× within minutes.
Verification (post-mitigation)¶
After adding swap or freeing memory:
ssh root@unraid 'echo "=== AFTER ==="; uptime; free -h; swapon --show
echo "=== VMSTAT ==="; vmstat 1 3
echo "=== IOSTAT ==="; iostat -xmN 1 3 | grep -E "^(nvme0n1|loop0) "
echo "=== UNHEALTHY ==="; docker ps --filter health=unhealthy --format "{{.Names}}: {{.Status}}"'
Success criteria:
- Load avg drops below 2× core count within 5 min
-
sysCPU < 10% - Page cache grows back above 6 GB
- Unhealthy container count returns to 0
-
vmstat bi< 10k/s with normal workload
Prevention checklist (post-incident)¶
- Swap configured (≥ 16 GB on vortex @swap NOCOW subvolume)
- Swap entry in
/boot/config/go(survives reboot) -
flowcollRSS monitored — alert if > 5 GB sustained (likely leak) - JVM containers have explicit
-Xmx(opensearch: 2g, others as appropriate) - No container without
restart: unless-stoppedor equivalent - Uptime Kuma tracks memory usage trend (or VictoriaMetrics alert:
(1 - mem_cache/RAM) < 0.2 AND sys_cpu > 0.5)
Related¶
- Unraid Skill —
## Troubleshooting: Memory/IO Pressuresection - Array Stop Stuck Runbook — different failure mode, similar decision-tree pattern
- Incident 2026-07-06: 31 GB RAM, 0 swap, 60+ containers → load 308 → 12 unhealthy → fixed via 32 GB swap on vortex