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."
/procis a virtual filesystem generated by the kernel in memory. Reading data is as simple asopen()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/stattick counts. psutil handles that arithmetic for you. - Container resilience. In strict Docker or sandboxed environments, some files in
/procare masked or restricted for security. Opening/proc/meminfomay throwFileNotFoundErrororPermissionError. 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