SYS.ACTIVE
Tharana-dev
CPU: 12% | MEM: 44%
[ PYTHON / SYSTEMS ]

When /proc Fails

Why I use psutil — and when I still prefer reading the kernel directly.

I was building my Telemetry Engine — the pure-Python system monitor that reads directly from /proc/stat and /proc/meminfo. It worked beautifully on Linux. Then I tried to run it on Windows and it broke instantly.

There is no /proc folder on Windows. This is what I learned while fixing that.

Two Different Countries

On Windows, the kernel doesn't expose text files. To find out CPU usage or memory, you have to call the Windows C-API (functions like GetSystemTimes or GlobalMemoryStatusEx) using ctypes, writing C-style structures from Python. It's possible, but it's not what I want to spend my weekend on.

Imagine you're traveling to Europe. You have a Sri Lankan laptop charger — three rectangular prongs. The European wall socket takes two round prongs. You can't plug it in. You need a travel adapter.

In Python, Linux is the Sri Lankan socket (/proc). Windows is the European socket (the Win32 C-API). They are fundamentally different. If you use your Linux code on Windows, it crashes.

psutil: The Universal Adapter

psutil (process and system utilities) is a library that gives you one Python API for CPU, memory, disk, network, and process metrics. Under the hood it does exactly what I just described: on Linux it parses /proc, on Windows it calls the C-API, on macOS it uses the Mach kernel.

You install it with pip install psutil and you stop worrying about which country you're in.

My Snapshot Pattern

Instead of returning raw dictionaries, I wrap psutil's data into frozen dataclasses. Frozen because a snapshot should be immutable — once you've captured "CPU at 3:42pm", it shouldn't be mutable by the caller. It's also thread-safe if you ever move to a multi-threaded renderer (which, spoiler, you eventually will).

import psutil
import time
from dataclasses import dataclass


@dataclass(frozen=True)
class PracticeCpuSnapshot:
    usage_pct: float


@dataclass(frozen=True)
class PracticeMemorySnapshot:
    total_gb: float
    used_gb: float
    used_pct: float
    available_gb: float

CPU: The interval Gotcha

This tripped me up more than anything else in psutil. psutil.cpu_percent() has two completely different modes depending on whether you pass an interval.

Mode 1: with interval

cpu = psutil.cpu_percent(interval=1)
print(cpu)
# 8.9 — pauses the code for 1 second, measures that window

This blocks the calling thread for the given number of seconds, then returns the average utilization over that window. Simple to reason about. But it freezes your program for the duration.

The downside shows up the moment you add more metrics: if you want to also track memory and disk inside the same loop, they'll all have to wait behind the CPU call.

Mode 2: without interval

# First call initializes the counter (returns 0.0 or a dummy value)
psutil.cpu_percent()

for i in range(5):
    time.sleep(1)              # Your program waits here
    cpu = psutil.cpu_percent()  # Measures the delta over that 1s sleep
    print(f"CPU Usage: {cpu}%")

# CPU Usage: 2.7%
# CPU Usage: 3.7%
# CPU Usage: 3.2%
# CPU Usage: 0.3%
# CPU Usage: 0.6%

Without an interval, psutil computes the delta between this call and the previous call (or module import, if it's the first call). Because the first call has no previous data point, it returns 0.0. That's the trap.

Why this is better for a live dashboard: the loop wakes up instantly. You can grab CPU, memory, and disk at the exact same moment without adding extra delays. If you want to show a smooth refresh, you control the timing yourself with time.sleep(), not psutil.

If you only take one thing from this post: cpu_percent(interval=1) is for one-off measurements. cpu_percent() inside a timed loop is for live monitoring.

For real-time dashboards, the pattern that scales is a two-thread model: one thread samples psutil on a timer, the other thread renders from a queue. Neither blocks the other, and the sampling thread can grab multiple metrics per tick. I'll write a dedicated post on that when I wire it into Telemetry Engine — for now, mode 2 is the simplest thing that works.

Memory: Bytes, Not Gigabytes

psutil.virtual_memory() returns a named tuple with everything you'd want — total, available, used, free, percent, buffers, cached. But every number is in bytes, not GB or KB.

ram = psutil.virtual_memory()
print(ram)

# svmem(
#     total=7964626944,
#     available=5300813824,
#     percent=33.4,
#     used=2663813120,
#     free=3398967296,
#     active=288079872,
#     inactive=3738865664,
#     buffers=80588800,
#     cached=2002554880,
#     shared=3932160,
#     slab=153993216
# )

Divide by 1024**3 for GB. Wrap it in your frozen dataclass and round it cleanly:

for i in range(5):
    mem = psutil.virtual_memory()

    total_gb     = round(mem.total     / (1024**3), 2)
    used_gb      = round(mem.used      / (1024**3), 2)
    available_gb = round(mem.available / (1024**3), 2)
    used_pct     = round(mem.percent, 2)   # already a percentage

    r = PracticeMemorySnapshot(
        total_gb=total_gb,
        used_gb=used_gb,
        used_pct=used_pct,
        available_gb=available_gb,
    )

    print(r)
    time.sleep(1)

# PracticeMemorySnapshot(total_gb=7.42, used_gb=2.41, used_pct=32.4, available_gb=5.01)
# PracticeMemorySnapshot(total_gb=7.42, used_gb=2.41, used_pct=32.5, available_gb=5.01)
# PracticeMemorySnapshot(total_gb=7.42, used_gb=2.44, used_pct=32.9, available_gb=4.98)

So Why Do Developers Still Prefer /proc on Linux?

If psutil works everywhere, why bother reading /proc at all? Three reasons:

  • Zero dependencies. No pip install psutil. Your code runs on any Linux box, even an air-gapped one, with nothing but the standard library.
  • Pure file I/O. In Linux, "everything is a file." /proc is a virtual filesystem generated by the kernel in memory. Reading data is as simple as open() and parsing.
  • Lightweight. No library wrapper overhead. For a tool whose whole point is to measure system performance, skipping a layer of Python around the raw data is philosophically cleaner.

And Why Use psutil as a Fallback?

Two reasons it's still worth keeping psutil around:

  • It simplifies complex math. For CPU usage over time, you have to do the delta calculation yourself from raw /proc/stat tick counts. psutil handles that arithmetic for you.
  • Container resilience. In strict Docker or sandboxed environments, some files in /proc are masked or restricted for security. Opening /proc/meminfo may throw FileNotFoundError or PermissionError. Falling back to psutil keeps the app running instead of crashing.

The Cross-Platform Pattern

Here's the shape of a cross-platform telemetry function. Linux takes the fast path; everything else uses psutil. Note: I check explicitly for linux, not "not Windows" — macOS has no /proc either, so it must fall through to psutil too.

import sys

if sys.platform.startswith("linux"):
    # Fast path: read directly from /proc on Linux
    try:
        with open("/proc/meminfo", "r") as f:
            # (parse /proc/meminfo here)
            pass
    except (PermissionError, FileNotFoundError):
        # Containerized or hardened Linux — /proc might be masked
        import psutil
        memory_pct = psutil.virtual_memory().percent
else:
    # Windows (win32), macOS (darwin), or anything else
    import psutil
    memory_pct = psutil.virtual_memory().percent

What I'm Taking Forward

psutil is not "better" or "worse" than /proc — they solve different problems. /proc is faster and dependency-free but only works on Linux. psutil is slower and needs an install but works everywhere and handles the math.

For a real production tool, the right answer is both: read /proc when you can, fall back to psutil when you can't. That's how I want Telemetry Engine to eventually look. One step at a time.

Tharana Nimsara

✅ VERIFIED AUTHOR

SHA256: b7d3e1...a9c4

ESC
tharana@dev: ~

Welcome to TharanaOS v1.0.0

Type 'help' to see available commands.

➜ ~
Hello, user.
(o_o)