<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Harmeet Singh</title>
    <description>The latest articles on DEV Community by Harmeet Singh (@harmeet10000).</description>
    <link>https://dev.to/harmeet10000</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1452813%2Fa6455d62-dfdd-4d34-8e16-9e9941c37410.jpeg</url>
      <title>DEV Community: Harmeet Singh</title>
      <link>https://dev.to/harmeet10000</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harmeet10000"/>
    <language>en</language>
    <item>
      <title>My Laptop Was Drowning in Swap. Fixing It Taught Me Three Things Linux Never Told Me.</title>
      <dc:creator>Harmeet Singh</dc:creator>
      <pubDate>Fri, 30 Oct 2026 15:55:50 +0000</pubDate>
      <link>https://dev.to/harmeet10000/my-laptop-was-drowning-in-swap-fixing-it-taught-me-three-things-linux-never-told-me-3lk0</link>
      <guid>https://dev.to/harmeet10000/my-laptop-was-drowning-in-swap-fixing-it-taught-me-three-things-linux-never-told-me-3lk0</guid>
      <description>&lt;p&gt;&lt;em&gt;7.5 GiB of RAM, three AI ingestion workers, a budget SSD, and a compressed swap cache that had been silently running the wrong algorithm for months. This is the story of a measured, reversible fix — and the two bugs hiding underneath it.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;My daily driver is an Ubuntu 26.04 laptop with an i5-8250U (8 threads), 7.5 GiB of RAM, and a no-name 256 GB SATA SSD — the kind of DRAM-less budget drive that has brutal random-4K latency. On top of it I run the worst possible workload for that hardware: a browser, VS Code, Postman, VLC, a desktop full of Electron apps, &lt;em&gt;and&lt;/em&gt; my own project — a LangChain/FastAPI platform with Celery ingestion workers that import PyTorch, transformers, and docling.&lt;/p&gt;

&lt;p&gt;The numbers looked dire at first glance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MemTotal:   7880296 kB    (~7.5 GiB)
MemAvailable: 1180008 kB  (~1.15 GiB)
SwapTotal: 10485756 kB
SwapFree:   5245240 kB    (~5 GiB of swap "in use")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five gigabytes of swap in use on a 7.5 GiB machine. The folk wisdom says: &lt;em&gt;install zram, thank me later.&lt;/em&gt; I very nearly did.&lt;/p&gt;

&lt;p&gt;Instead, I spent an evening measuring before changing anything, and that decision re-shaped the entire fix. Three times over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson zero: the number you trust may be wearing a costume
&lt;/h2&gt;

&lt;p&gt;The first thing to understand is what &lt;code&gt;swapon --show&lt;/code&gt; actually reports. That "5 GiB used" does &lt;strong&gt;not&lt;/strong&gt; mean 5 GiB of pages sitting on my SSD. It counts &lt;strong&gt;allocated swap slots&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's why that matters: my kernel had &lt;strong&gt;zswap&lt;/strong&gt; enabled — a compressed RAM cache sitting in front of the disk swap. When the kernel evicts an anonymous page with zswap on, the page is compressed into a RAM pool, and a slot on the backing device is allocated as a placeholder. The page itself often never touches the disk at all.&lt;/p&gt;

&lt;p&gt;So how much of my "5 GiB of disk swap" was actually on disk? The kernel's own counters gave the answer away: over 53 hours of uptime, &lt;code&gt;pswpout&lt;/code&gt; recorded ~6.4M pages written (~25 GiB, mostly bursts), but pressure stall information (PSI) for memory was near zero at idle, and — the killer detail, which I extracted later — the zswap pool stats showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stored_pages      = 288076      (~1.13 GiB of pages)
pool_total_size   = 453316608   (~432 MiB compressed)
written_back_pages = 1059       (!)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 2.5:1 compression ratio, and barely a thousand pages ever written back to the SSD by the shrinker. The disk was almost never in the loop. What looked like "drowning in swap" was actually a working compressed-cache setup — one that deserved credit, not replacement.&lt;/p&gt;

&lt;p&gt;This also corrected my own first diagnosis. I initially read "5 GiB swap used" as disk pressure. The counters said otherwise. &lt;strong&gt;Measure before you tune. The kernel leaves honest breadcrumbs — &lt;code&gt;/proc/vmstat&lt;/code&gt;, &lt;code&gt;/proc/pressure/*&lt;/code&gt;, and zswap's debugfs stats will out-argue any dashboard.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson one: your config can be silently ignored
&lt;/h2&gt;

&lt;p&gt;The second discovery came from comparing two files that should have agreed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/cmdline
... zswap.enabled&lt;span class="o"&gt;=&lt;/span&gt;1 zswap.compressor&lt;span class="o"&gt;=&lt;/span&gt;lz4 quiet

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/module/zswap/parameters/compressor
lzo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My kernel command line &lt;em&gt;demanded&lt;/em&gt; LZ4. The live system was running &lt;strong&gt;LZO&lt;/strong&gt; — a compression algorithm roughly 30–40% slower per byte, for months, without a single loud warning.&lt;/p&gt;

&lt;p&gt;The mechanism: &lt;code&gt;CONFIG_CRYPTO_LZ4=m&lt;/code&gt; in Ubuntu's kernel means LZ4 support is a &lt;em&gt;loadable module&lt;/em&gt;. zswap resolves its compressor when the first swap device registers, very early in boot — before anything has loaded &lt;code&gt;crypto_lz4&lt;/code&gt;. The request fails, zswap falls back to its compiled-in default (LZO), and &lt;strong&gt;never retries the value from the cmdline&lt;/strong&gt;. No dmesg sirens. Just a quiet downgrade.&lt;/p&gt;

&lt;p&gt;The fix is two lines — one for now, one for every boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;modprobe lz4
&lt;span class="nb"&gt;echo &lt;/span&gt;lz4 | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /sys/module/zswap/parameters/compressor
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"lz4"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/modules-load.d/lz4.conf   &lt;span class="c"&gt;# persists across reboots&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've ever "enabled lz4" on a Linux box, go check right now that the live parameter matches your intention. On a shocking number of machines, they don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson two: zram and zswap are rivals, not teammates
&lt;/h2&gt;

&lt;p&gt;With the cache-audit done, the actual decision was architectural. Both zram and zswap compress evicted pages into RAM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;zswap&lt;/strong&gt; is a cache &lt;em&gt;in front of&lt;/em&gt; a backing swap device (mine: a 10 GiB swap file on the SSD).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;zram&lt;/strong&gt; &lt;em&gt;is&lt;/em&gt; the swap device — a compressed RAM disk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since kernel 6.12's zswap shrinker, the folk wisdom "zram &amp;gt; zswap" is mostly obsolete: zswap-with-disk now behaves functionally like zram-with-writeback. The remaining deltas are fault latency (µs-scale decompression vs. random 4K reads on my SSD) and SSD wear — both of which matter on my hardware, so a zram trial was justified.&lt;/p&gt;

&lt;p&gt;But here's the trap that shapes the whole migration: &lt;strong&gt;zswap intercepts writes to &lt;em&gt;all&lt;/em&gt; swap devices, including zram.&lt;/strong&gt; Enable both and every page gets compressed twice — CPU burned for nothing, accounting garbage. They are alternatives, not teammates. Adopting zram means disabling zswap, full stop.&lt;/p&gt;

&lt;p&gt;I asked whether zswap could instead be scoped "hierarchically" — a compressed front-tier with a threshold handing off to zram. It can't; zswap has no per-device scoping. But the hierarchy I actually wanted already existed one layer down: &lt;strong&gt;swap priorities&lt;/strong&gt;. Give zram priority 100 and the swap file priority −1, and the kernel fills zram first, only overflowing to the SSD past its capacity threshold. Tiering was built into the swap subsystem all along.&lt;/p&gt;

&lt;p&gt;So the plan, in phases, each reversible:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Phase 0&lt;/strong&gt; — make LZ4 real for the existing zswap, tune &lt;code&gt;vm.swappiness=100&lt;/code&gt; and &lt;code&gt;vm.page-cluster=0&lt;/code&gt;, and cap the Celery workers' memory appetite (more on that later).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1&lt;/strong&gt; — install &lt;code&gt;systemd-zram-generator&lt;/code&gt;, configure 4 GiB of LZ4 zram at priority 100, disable zswap live &lt;em&gt;before&lt;/em&gt; zram starts (ordering matters), and flip the GRUB cmdline to &lt;code&gt;zswap.enabled=0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;/swap.img&lt;/code&gt; untouched as the overflow tier. No migration tricks — with 1.15 GiB free, a &lt;code&gt;swapoff&lt;/code&gt; on the file would OOM the box; natural churn redistributes over days.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The plot thickens: two bugs hiding in "it worked"
&lt;/h2&gt;

&lt;p&gt;Phase 0 + Phase 1 ran as one script. The verification output looked almost perfect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME       TYPE      SIZE USED PRIO
/swap.img  file       10G 1.6G   -1
/dev/zram0 partition 3.8G   0B  100

/dev/zram0 lzo-rle       3.8G   4K   80B ...
zswap enabled : N   (want 0)
swappiness    : 100 (want 100)
page-cluster  : 0   (want 0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything correct — except &lt;strong&gt;&lt;code&gt;lzo-rle&lt;/code&gt; where LZ4 was promised&lt;/strong&gt;. The generator config was unambiguous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[zram0]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;zram-size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;4096&lt;/span&gt;
&lt;span class="py"&gt;compression-algorithm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;lz4&lt;/span&gt;
&lt;span class="py"&gt;swap-priority&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the service had exited &lt;code&gt;0/SUCCESS&lt;/code&gt;. A unit that exits green while doing the wrong thing logs nothing — which is exactly why nobody notices this class of bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug the first: the hot-add ABI moved.&lt;/strong&gt; My diagnostic script read &lt;code&gt;/sys/class/zram-control&lt;/code&gt; to create a throwaway device — the standard incantation for a decade. On this kernel it answered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat: /sys/class/zram-control: Is a directory.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern kernels turned the file into a directory; hot-add now lives at &lt;code&gt;/sys/class/zram-control/hot_add&lt;/code&gt;. Because the failure mode is a filesystem error rather than a permissions error, it reads as "something's broken" rather than "the interface moved." Script defensively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /sys/class/zram-control &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;ZID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/class/zram-control&lt;span class="si"&gt;)&lt;/span&gt;          &lt;span class="c"&gt;# pre-6.x ABI&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nv"&gt;ZID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/class/zram-control/hot_add&lt;span class="si"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;# 6.x+ ABI&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bug the second: the generator silently skips the algorithm write.&lt;/strong&gt; With the hot-add fixed, I ran the kernel-acceptance probe on a throwaway device — &lt;code&gt;echo lz4 &amp;gt; comp_algorithm&lt;/code&gt;, read it back: accepted. So the kernel wants LZ4, the config requests LZ4, the binary contains the config key... and the device still comes up &lt;code&gt;lzo-rle&lt;/code&gt;. Root cause by strace — trace the generator's actual sysfs writes during setup. The syscall log shows whether it ever opens and writes &lt;code&gt;comp_algorithm&lt;/code&gt; at all. (It doesn't need to &lt;em&gt;fail loudly&lt;/em&gt; to be wrong; it just needs to skip.)&lt;/p&gt;

&lt;p&gt;The recovery was to stop trusting the tool and build the setup I could verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;swapoff /dev/zram0
&lt;span class="nb"&gt;echo &lt;/span&gt;1 | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /sys/block/zram0/reset
&lt;span class="nb"&gt;echo &lt;/span&gt;lz4 | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /sys/block/zram0/comp_algorithm   &lt;span class="c"&gt;# before disksize — EBUSY after!&lt;/span&gt;
&lt;span class="nb"&gt;echo &lt;/span&gt;4294967296 | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /sys/block/zram0/disksize
mkswap &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; zram0 /dev/zram0
swapon &lt;span class="nt"&gt;-p&lt;/span&gt; 100 /dev/zram0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two ordering laws matter here: &lt;strong&gt;algorithm before disksize&lt;/strong&gt; (writes after sizing return EBUSY, and the kernel quietly keeps its compiled-in default), and &lt;strong&gt;mkswap before swapon&lt;/strong&gt;. Then, for durability, a small oneshot systemd unit (&lt;code&gt;Before=swap.target&lt;/code&gt;, idempotent) replaces the generator's setup at boot, and the generator config is retired to &lt;code&gt;.disabled&lt;/code&gt;. Now the boot-time behavior is deterministic and LZ4 is &lt;em&gt;enforced&lt;/em&gt;, not requested.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other half: why the workers ate 2.3 GiB in the first place
&lt;/h2&gt;

&lt;p&gt;Fixing swap was treating the symptom. The disease: three Celery prefork processes with ~780 MB swapped out &lt;em&gt;each&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This turned out to be structural, not a leak. The ingestion workers import &lt;strong&gt;torch 2.9.1 + transformers + docling + langextract&lt;/strong&gt; at startup. That's a ~0.5–0.9 GiB &lt;em&gt;baseline&lt;/em&gt; per child before any task state exists. Prefork makes this survivable — children fork from a warm master and inherit everything copy-on-write — but nothing bounded how much &lt;em&gt;task state&lt;/em&gt; each child could accumulate before recycling. The profile already had &lt;code&gt;max_tasks_per_child=100&lt;/code&gt;; a hundred memory-hungry tasks is a long life.&lt;/p&gt;

&lt;p&gt;Celery has a purpose-built control for exactly this: &lt;code&gt;--max-memory-per-child&lt;/code&gt; (KiB; the child is recycled when RSS crosses it). I wired it through the codebase the way the architecture demands — settings → profile → CLI — so the flag is profile-owned and operators can't override it per-invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="n"&gt;CELERY_DEFAULT_MAX_MEMORY_PER_CHILD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CELERY_INGESTION_MAX_MEMORY_PER_CHILD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CELERY_CRAWLER_MAX_MEMORY_PER_CHILD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cli.py — emitted from the profile, and added to the forbidden-override set
&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--max-memory-per-child&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_memory_per_child&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because children fork from the warm master, recycling is cheap — the "recycling is expensive" intuition belongs to thread/solo pools, not prefork. The contract tests were extended so the flag stays pinned to the profile forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verdict so far
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;zramctl
NAME       ALGORITHM DISKSIZE  DATA   COMPR     TOTAL
/dev/zram0 lz4          3.8G   ...   ...       ...
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/block/zram0/mm_stat
...   &lt;span class="c"&gt;# orig_data_size / compr_data_size = the effective ratio, in a few days&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I expect, and what I'll judge it by — not vibes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App/tab switching after idle&lt;/strong&gt; gets visibly faster. Evicted pages come back as LZ4 decompressions in RAM instead of random 4K reads from a DRAM-less SSD. That stutter was the complaint that started all of this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory-pressure moments&lt;/strong&gt; (agent spawns, Chrome ballooning) degrade more gracefully — eviction is cheap, so the kernel stalls less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The SSD stops paying the swap tax&lt;/strong&gt; until zram's 4 GiB fills.&lt;/li&gt;
&lt;li&gt;When the &lt;em&gt;active&lt;/em&gt; working set exceeds RAM+zram, nothing virtualizes that. This fix removes the tax of swapping; it doesn't conjure capacity. On this machine, the honest ceiling was always the three torch-importing workers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The metrics I'm watching for a verdict: &lt;code&gt;orig_data_size / compr_data_size&lt;/code&gt; from &lt;code&gt;mm_stat&lt;/code&gt; (expect ~2.5:1 based on the zswap evidence), the growth rate of &lt;code&gt;workingset_refault_anon&lt;/code&gt; in &lt;code&gt;/proc/vmstat&lt;/code&gt;, and memory PSI during my normal day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules I'd hand to a friend
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;swapon --show&lt;/code&gt; "used" counts slots, not disk bytes.&lt;/strong&gt; On any zswap machine, that number is mostly RAM wearing a disk costume. Check &lt;code&gt;pswpin/pswpout&lt;/code&gt; and &lt;code&gt;/sys/kernel/debug/zswap/*&lt;/code&gt; before believing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configs can be silently downgraded.&lt;/strong&gt; Compare &lt;code&gt;/proc/cmdline&lt;/code&gt; against &lt;code&gt;/sys/module/zswap/parameters/*&lt;/code&gt;. Trust the read-back, not the write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;zram and zswap are alternatives.&lt;/strong&gt; Both on means double compression. The tiering you want is swap priority: zram high, disk low.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A green exit code proves nothing.&lt;/strong&gt; The generator exited 0 while skipping its job. When config "doesn't take", strace the sysfs writes — units that lie don't log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kernel interfaces move under your feet.&lt;/strong&gt; &lt;code&gt;/sys/class/zram-control&lt;/code&gt; went from file to directory. Probe, don't assume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ordering is law with zram&lt;/strong&gt;: algorithm → disksize → mkswap → swapon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix causes too.&lt;/strong&gt; Swap tuning made the symptoms cheaper; &lt;code&gt;--max-memory-per-child&lt;/code&gt; made the workers stop hoarding. Do both.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;All of this was done reversibly: kernel modules, sysctl files, a generator config, one GRUB word (with a backup), and a systemd unit. Every step had a rollback. That's not timidity — that's what makes the measurement trustworthy.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[Insert final mm_stat ratio and refault-counter comparison after the trial period.]&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
