{"slug": "ratatop-day-2-the-memory-box-and-the-lie-in-free-h", "title": "ratatop day 2: the memory box, and the lie in `free -h`", "summary": "A developer building ratatop, a Rust system monitor with Ratatui, discovered that parsing /proc/meminfo for memory stats is fraught with pitfalls. The MemFree field is misleading because Linux aggressively uses free RAM for caching, making it trend toward zero on healthy systems; the correct metric is MemAvailable. The developer also warns that SwapCached can be mistaken for Cached if parsing is not exact, and that the 'kB' suffix actually means kibibytes.", "body_md": "*Hello, I'm Maneshwar. I'm building git-lrc, a Micro AI code reviewer that runs on every commit. It is free and source-available on Github. Star git-lrc to help devs discover the project. Do give it a try and share your feedback.*\n\n[Yesterday](https://dev.to/lovestaco/building-ratatop-a-rust-system-monitor-with-ratatui-50n) I got the CPU box working in [ratatui](https://ratatui.rs) and wrote up my first impressions coming from Bubble Tea.\n\nToday: memory.\n\nI assumed this would be the easy one.\n\nCPU needed delta tracking between samples, per-core bookkeeping, braille bit twiddling. Memory is just a file with numbers in it.\n\nRead it, print it, go home.\n\nReader, it was not the easy one.\n\nLook at that box again. Total 13.5 GiB. Free: 271 MiB.\n\nTwo percent free. That sounds like a machine about to fall over.\n\nIt is completely fine. I have 7.10 GiB available right now.\n\nThis is the thing that trips up everyone reading `/proc/meminfo`\n\nfor the first time, and it tripped me up too.\n\n`MemFree`\n\nmeans memory the kernel is doing absolutely nothing with. Not caching a file, not buffering a write, nothing. Just sitting there.\n\nAnd the kernel hates that. Unused RAM is wasted RAM. So it fills spare memory with page cache, and hands it back the instant a process actually needs it.\n\nSo `MemFree`\n\non a healthy machine trends toward zero, by design. It is a measure of how much RAM your kernel is failing to put to work.\n\nThe number you actually want is `MemAvailable`\n\n, added to the kernel in 3.14 precisely because everyone was misreading `MemFree`\n\n.\n\nIt is an estimate of how much you can allocate right now without swapping, and it accounts for the fact that most of that cache is reclaimable.\n\nNote that `Used`\n\nand `Available`\n\nadd up to `Total`\n\n, but `Cached`\n\nand `Free`\n\ndo not slot in neatly beside them.\n\nCached memory is counted inside available. They overlap.\n\nTrying to make all four sum to total is a trap, and I definitely tried for a bit.\n\nMeme 1\n\nTemplate:Bernie Sanders \"I am once again asking\"\n\nText: \"I am once again asking you to stop reading MemFree\"\n\nHere is where I went looking at how [btop](https://github.com/aristocratos/btop) does it instead of inventing my own.\n\nThe obvious approach is to add up the parts.\n\nTotal minus free minus cached minus buffers, something like that.\n\nThis is what a lot of tools did historically, and it is where the old \"Linux ate my RAM\" confusion comes from.\n\nbtop does the simple thing instead:\n\n```\nused = MemTotal - MemAvailable\n```\n\nThat is it. Let the kernel do the hard estimate, because it is the only thing that actually knows what is reclaimable.\n\nIn Rust, my collector boils down to this:\n\n```\npub struct Memory {\n    pub total: u64,\n    pub used: u64,\n    pub available: u64,\n    pub cached: u64,\n    pub free: u64,\n}\n```\n\nPlain `u64`\n\nbytes, no units baked in. Formatting is the UI's problem, not the collector's.\n\n`/proc/meminfo`\n\nlooks like this:\n\n```\nMemTotal:       14177796 kB\nMemFree:          497404 kB\nMemAvailable:    7490900 kB\nBuffers:         1235656 kB\nCached:          5146132 kB\nSwapCached:        81068 kB\n```\n\nSpot the landmine.\n\n`SwapCached`\n\ncontains the substring `Cached`\n\n.\n\nIf you parse this with a `contains`\n\nor a loose prefix match, `SwapCached`\n\narrives four lines later and quietly overwrites your real cached value with something roughly 60 times smaller.\n\nYour box renders. No error. No panic. Just a wrong number sitting there looking plausible.\n\nThe fix is boring: split on the colon, match the label exactly.\n\n``` js\nlet Some((label, rest)) = line.split_once(':') else {\n    continue;\n};\n\nmatch label {\n    \"MemTotal\" => mem.total = bytes,\n    \"MemFree\" => mem.free = bytes,\n    \"MemAvailable\" => available = Some(bytes),\n    \"Cached\" => mem.cached = bytes,\n    _ => {}\n}\n```\n\nI wrote a test named `swap_cached_does_not_get_mistaken_for_cached`\n\npurely so the next person who \"simplifies\" this parser gets caught.\n\nAlso worth knowing: the `kB`\n\nsuffix in that file is a lie too. Those are KiB, 1024 bytes, not 1000. So the multiply is `<< 10`\n\n, not `* 1000`\n\n.\n\nLook at the value column again:\n\n```\n13.5 GiB\n6.42 GiB\n 271 MiB\n```\n\nDifferent decimal counts. That is deliberate, and it is what btop does.\n\nThe rule is three significant figures, always. Under 10, you get two decimals. Between 10 and 100, one. Above 100, none. The number is always exactly three digits wide, so the column stays aligned no matter what the values do.\n\n``` js\nlet decimals = if unit == 0 || value >= 100.0 {\n    0\n} else if value >= 10.0 {\n    1\n} else {\n    2\n};\n```\n\nSmall thing. But a column that jitters between `6.4 GiB`\n\nand `13.52 GiB`\n\nlooks amateurish in a way you feel before you can name it.\n\nOne thing I got wrong on the first pass: my loop divided by 1024 while the value was too large, with no guard on the unit table.\n\nFeed that a `u64::MAX`\n\nand it walks straight off the end of the array.\n\nNow it stops at TiB, with a test that passes `u64::MAX`\n\nspecifically.\n\nTwo things stood out building a second box.\n\n**Each field gets its own gradient.** btop's theme gives used, available, cached and free their own three-stop colour ramps, and I lifted the exact hex values out of `btop_theme.cpp`\n\n.\n\nBecause a `Gradient`\n\nis just a value in my code, colouring a meter is passing a different one in:\n\n```\nfor (label, value, gradient) in [\n    (\"Used:\",      mem.used,      theme::MEM_USED),\n    (\"Available:\", mem.available, theme::MEM_AVAILABLE),\n    (\"Cached:\",    mem.cached,    theme::MEM_CACHED),\n    (\"Free:\",      mem.free,      theme::MEM_FREE),\n] {\n    render_value(inner, &mut y, label, value, buf);\n    render_meter(inner, &mut y, mem.percent(value), gradient, buf);\n}\n```\n\nImmediate mode really pays off here.\n\nThere is no widget to construct, register, style and later mutate.\n\nYou are in a loop, you have a `Rect`\n\n, you draw. Four fields, four iterations, done.\n\n**The meter widget was already written.** I built it for the CPU bar on day one, and reusing it for memory took zero changes.\n\nThat is the nice consequence of ratatui's `Widget`\n\ntrait being tiny: a widget is a struct plus one `render`\n\nmethod, so a generic one you wrote yesterday just works today.\n\nI did briefly add a `Meter::braille()`\n\nmethod to make the memory bars look different. Then I noticed it returned `self`\n\ncompletely unchanged, doing nothing but making the call site read fancier. Deleted it. A method that lies about doing something is worse than no method.\n\nDay one had everything in one `ui.rs`\n\n. That was fine for one box and immediately wrong for two.\n\n```\nflowchart TD\n    M[\"ui/mod.rs<br/>splits the terminal, paints background\"]\n    M --> C[\"ui/cpu.rs<br/>bar, cores, load\"]\n    M --> Mem[\"ui/mem.rs<br/>values, meters, formatting\"]\n    C --> W[\"widgets/<br/>BrailleGraph, Meter\"]\n    Mem --> W\n    C --> Col[\"collect/cpu.rs\"]\n    Mem --> Col2[\"collect/mem.rs\"]\n```\n\nThe useful change was moving layout decisions *up*.\n\nPreviously `main.rs`\n\ncomputed the box rectangle itself, which was fine when there was exactly one box and completely wrong the moment memory needed the leftover height.\n\nNow `main.rs`\n\nhands over the entire terminal and `ui::render`\n\ndecides who gets what:\n\n```\nlet [cpu_area, rest] = Layout::vertical([\n    Constraint::Length(cpu_height),\n    Constraint::Min(0),\n])\n.areas(area);\n```\n\nCPU takes the top, memory takes a fixed width below it, and the rest stays empty for the network and process boxes.\n\nAdding a box is now a matter of carving another `Rect`\n\nout of `rest`\n\n.\n\nNetwork, probably. `/proc/net/dev`\n\nis another counter file, so it needs the same delta treatment the CPU box uses, plus auto-scaling units because network speeds swing across five orders of magnitude.\n\nThen processes, which is the big one. That is where `/proc/[pid]/stat`\n\nlives, and where I suspect I will learn a great deal about how many edge cases a process table has.\n\nCode so far: [lovestaco/ratatop](https://github.com/lovestaco/ratatop).\n\nIf you take one thing from this post, make it this: stop looking at `free`\n\n. Look at `available`\n\n. Your machine is fine.\n\nAI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.\n\ngit-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.\n\nAny feedback or contributors are welcome! It's online, source-available, and ready for anyone to use.\n\n⭐ Star it on GitHub:\n\n| [🇩🇰 Dansk](https://github.com/HexmosTech/git-lrc/readme/README.da.md) | [🇪🇸 Español](https://github.com/HexmosTech/git-lrc/readme/README.es.md) | [🇮🇷 Farsi](https://github.com/HexmosTech/git-lrc/readme/README.fa.md) | [🇫🇮 Suomi](https://github.com/HexmosTech/git-lrc/readme/README.fi.md) | [🇯🇵 日本語](https://github.com/HexmosTech/git-lrc/readme/README.ja.md) | [🇳🇴 Norsk](https://github.com/HexmosTech/git-lrc/readme/README.nn.md) | [🇵🇹 Português](https://github.com/HexmosTech/git-lrc/readme/README.pt.md) | [🇷🇺 Русский](https://github.com/HexmosTech/git-lrc/readme/README.ru.md) | [🇦🇱 Shqip](https://github.com/HexmosTech/git-lrc/readme/README.sq.md) | [🇨🇳 中文](https://github.com/HexmosTech/git-lrc/readme/README.zh.md) | [🇮🇳 हिन्दी](https://github.com/HexmosTech/git-lrc/readme/README.hi.md) |\n\nGenAI today is a **race car without brakes**. It accelerates fast -- you describe something, and large blocks of code appear instantly. But AI agents *silently break things*: they remove logic, relax constraints, introduce expensive cloud calls, leak credentials, and change behavior -- without telling you. You often find out in production.\n\n** git-lrc is your braking system.** It hooks into\n\n`git commit`\n\nand runs an AI review on every diff In short, git-lrc helps **Prevent Outages, Breaches, and Technical Debt Before They Happen**\n\n**At a glance:** [10 risk categories](https://github.com/HexmosTech/git-lrc#what-git-lrc-checks-for) · [100+ failure patterns tracked](https://github.com/HexmosTech/git-lrc#what-git-lrc-checks-for) · every commit…", "url": "https://wpnews.pro/news/ratatop-day-2-the-memory-box-and-the-lie-in-free-h", "canonical_source": "https://dev.to/lovestaco/ratatop-day-2-the-memory-box-and-the-lie-in-free-h-h67", "published_at": "2026-07-29 22:08:04+00:00", "updated_at": "2026-07-29 22:33:27.996126+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["ratatop", "Ratatui", "Linux", "/proc/meminfo", "btop"], "alternates": {"html": "https://wpnews.pro/news/ratatop-day-2-the-memory-box-and-the-lie-in-free-h", "markdown": "https://wpnews.pro/news/ratatop-day-2-the-memory-box-and-the-lie-in-free-h.md", "text": "https://wpnews.pro/news/ratatop-day-2-the-memory-box-and-the-lie-in-free-h.txt", "jsonld": "https://wpnews.pro/news/ratatop-day-2-the-memory-box-and-the-lie-in-free-h.jsonld"}}