Is the server running out of memory? Which process is eating the CPU? Is the disk nearly full before tonight's backup? You can answer all of these from Python with psutil (process and system utilities), a small cross-platform library that reads CPU, memory, disk, network and process information on Windows, Linux and macOS through one API. In this tutorial we print a system overview, list the top processes by memory, start and watch a child process and then stop it cleanly, measure live network and disk speed, and finish with a tiny health monitor that logs to CSV and flags alerts. Everything was tested on Python 3.13.5 with psutil 7.2.2, and every output block below is the real output of the script above it (from an 8-core Linux machine, so your numbers will differ).
Why psutil in 2026?
- One API for every OS: no parsing of
top,free,dfortasklistoutput; the same call works on Windows, Linux and macOS. - System and process level: machine-wide CPU, RAM, swap, disks and network, plus per-process memory, CPU, threads, open files and children.
- Prebuilt wheels:
pip install psutilinstalls a compiled wheel on all mainstream platforms, so there is nothing to build. - Python 3 only: the 7.x series dropped Python 2.7 support, so old snippets with Python 2 syntax will not apply; the API used here is current.
- Used everywhere: monitoring agents, test runners, Jupyter extensions and task queues use psutil under the hood, so the skills transfer.
Install
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install psutil
python -c "import psutil; print(psutil.__version__)"
psutil has no dependencies. Everything else in this guide (csv, datetime, time) comes from the standard library.
Step 1: CPU, RAM, disk and uptime overview
These five calls are the core of most monitoring scripts. Note the interval=1 in cpu_percent(): psutil compares two snapshots, so it blocks for one second and returns the real average. Without an interval, the first call returns a meaningless 0.0.
import datetime as dt
import psutil
GB = 1024 ** 3
print("CPU cores (logical/physical):", psutil.cpu_count(), "/", psutil.cpu_count(logical=False))
print("CPU usage over 1 s:", psutil.cpu_percent(interval=1), "%")
mem = psutil.virtual_memory()
print(f"RAM: {mem.used / GB:.1f} GB used of {mem.total / GB:.1f} GB "
f"({mem.percent}%), available {mem.available / GB:.1f} GB")
disk = psutil.disk_usage("/")
print(f"Disk /: {disk.used / GB:.0f} GB used of {disk.total / GB:.0f} GB ({disk.percent}%)")
boot = dt.datetime.fromtimestamp(psutil.boot_time())
uptime = dt.datetime.now() - boot
print("Uptime:", str(uptime).split(".")[0])
CPU cores (logical/physical): 8 / 8
CPU usage over 1 s: 1.6 %
RAM: 3.0 GB used of 15.6 GB (19.0%), available 12.7 GB
Disk /: 95 GB used of 127 GB (74.5%)
Uptime: 1 day, 0:56:07
For "how much memory is free", use available, not free. On Linux, free excludes cache that the kernel can release instantly, so it looks alarmingly low on a healthy machine. On Windows, pass a drive such as psutil.disk_usage("C:\\") instead of "/".
Step 2: List running processes and find the memory hogs
psutil.process_iter() yields every process. Passing a list of attribute names fetches them in one efficient call and stores them in p.info. Processes can exit while you loop, and some system processes deny access, so always guard the loop.
import psutil
MB = 1024 ** 2
procs = []
for p in psutil.process_iter(["pid", "name", "memory_info", "num_threads"]):
try:
procs.append((p.info["memory_info"].rss, p.info["pid"],
p.info["name"], p.info["num_threads"]))
except (psutil.NoSuchProcess, psutil.AccessDenied, AttributeError):
continue # process ended or is protected
print(f"{len(procs)} processes running. Top 5 by memory (RSS):")
print(f"{'PID':>7} {'NAME':<16}{'RSS MB':>9}{'THREADS':>9}")
for rss, pid, name, threads in sorted(procs, reverse=True)[:5]:
print(f"{pid:>7} {name[:15]:<16}{rss / MB:>9.1f}{threads:>9}")
126 processes running. Top 5 by memory (RSS):
PID NAME RSS MB THREADS
1595650 node 934.6 12
1587719 node 260.5 11
6946 orbitd 175.0 9
5673 node 149.8 11
423 node 122.2 11
When you request attributes this way, a value you are not allowed to read comes back as None instead of raising, which is why AttributeError is caught too. RSS (resident set size) is the physical memory the process uses right now, the same figure Task Manager and top show.
Step 3: Start, watch and stop a process
psutil.Popen works like subprocess.Popen but also gives you every Process method. Here we launch a child Python process that keeps one core busy and holds about 80 MB, sample it three times, then terminate it and wait for it to exit.
import sys, time
import psutil
# Start a child Python process that burns CPU and holds ~80 MB of RAM
code = "import time; data = bytearray(80 * 1024 * 1024)\nwhile True: sum(range(10000))"
child = psutil.Popen([sys.executable, "-c", code])
time.sleep(1) # let it allocate memory
child.cpu_percent(None) # first call primes the counter
for i in range(3):
time.sleep(1)
rss = child.memory_info().rss / 1024 ** 2
print(f"sample {i + 1}: pid={child.pid} status={child.status()} "
f"cpu={child.cpu_percent(None):.0f}% rss={rss:.0f} MB")
child.terminate() # ask it to stop (SIGTERM)
gone, alive = psutil.wait_procs([child], timeout=3)
print("stopped:", [p.pid == child.pid for p in gone], "still alive:", len(alive))
print("exists now?", psutil.pid_exists(child.pid))
sample 1: pid=1654435 status=running cpu=104% rss=88 MB
sample 2: pid=1654435 status=running cpu=100% rss=88 MB
sample 3: pid=1654435 status=running cpu=99% rss=88 MB
stopped: [True] still alive: 0
exists now? False
Two details matter. First, the first cpu_percent(None) call only primes the counter; later calls measure since the previous one. Second, process CPU is measured per core, so a busy single-threaded process reads about 100%, and the 104% sample is normal timing jitter; on an 8-core machine a fully loaded process could show up to 800%. For a process you did not start, use psutil.Process(pid), and prefer terminate() followed by wait_procs() before falling back to kill().
Step 4: Measure live network and disk speed
net_io_counters() and disk_io_counters() return running totals since boot. To get speed, take two snapshots and divide the difference by the elapsed time.
import time
import psutil
net1, disk1 = psutil.net_io_counters(), psutil.disk_io_counters()
time.sleep(2)
net2, disk2 = psutil.net_io_counters(), psutil.disk_io_counters()
kb = lambda b: b / 1024 / 2 # bytes over 2 s -> KB/s
print(f"network: down {kb(net2.bytes_recv - net1.bytes_recv):.1f} KB/s, "
f"up {kb(net2.bytes_sent - net1.bytes_sent):.1f} KB/s")
print(f"disk: read {kb(disk2.read_bytes - disk1.read_bytes):.1f} KB/s, "
f"write {kb(disk2.write_bytes - disk1.write_bytes):.1f} KB/s")
nics = [n for n, s in psutil.net_if_stats().items() if s.isup]
print("interfaces up:", ", ".join(sorted(nics)))
network: down 8.6 KB/s, up 8.9 KB/s
disk: read 0.0 KB/s, write 62.0 KB/s
interfaces up: enp0s3, lo
Pass pernic=True or perdisk=True to get one entry per network card or disk. On some containers and virtual machines disk_io_counters() can return None, so check for that in production code.
Step 5: A tiny health monitor that logs to CSV
Put it together: sample CPU, RAM and disk once per second, write each sample to a CSV file and label any row that crosses a threshold. Run it from Task Scheduler or cron with a longer loop, and you have a lightweight monitor with no agent to install.
import csv, datetime as dt
import psutil
CPU_LIMIT, MEM_LIMIT = 85.0, 90.0 # alert thresholds in %
with open("health_log.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["time", "cpu_pct", "mem_pct", "disk_pct", "alert"])
for _ in range(5):
cpu = psutil.cpu_percent(interval=1) # one sample per second
mem = psutil.virtual_memory().percent
disk = psutil.disk_usage("/").percent
alert = "HIGH CPU" if cpu > CPU_LIMIT else "HIGH RAM" if mem > MEM_LIMIT else ""
w.writerow([dt.datetime.now().strftime("%H:%M:%S"), cpu, mem, disk, alert])
rows = list(csv.DictReader(open("health_log.csv")))
for r in rows:
print(r["time"], f"cpu={r['cpu_pct']}%", f"mem={r['mem_pct']}%",
f"disk={r['disk_pct']}%", r["alert"] or "ok")
print("rows logged:", len(rows), "| max cpu:", max(float(r["cpu_pct"]) for r in rows), "%")
00:57:46 cpu=1.0% mem=19.2% disk=74.5% ok
00:57:47 cpu=1.0% mem=19.1% disk=74.5% ok
00:57:48 cpu=0.4% mem=19.1% disk=74.5% ok
00:57:49 cpu=0.7% mem=19.1% disk=74.5% ok
00:57:50 cpu=1.0% mem=19.1% disk=74.5% ok
rows logged: 5 | max cpu: 1.0 %
The machine was idle, so every row says ok. In a real job, replace the CSV write or add a step that e-mails you when alert is not empty, and keep the thresholds in a config file so you can tune them without editing code.
Quick reference
| Task | psutil call |
|---|---|
| CPU usage (%) | psutil.cpu_percent(interval=1) |
| Per-core usage | psutil.cpu_percent(interval=1, percpu=True) |
| Core count | psutil.cpu_count(), cpu_count(logical=False) |
| RAM total / available | psutil.virtual_memory() |
| Swap | psutil.swap_memory() |
| Disk space | psutil.disk_usage("/") |
| Network / disk totals | psutil.net_io_counters(), psutil.disk_io_counters() |
| All processes | psutil.process_iter(["pid", "name"]) |
| One process | psutil.Process(pid) |
| Stop processes | p.terminate() + psutil.wait_procs([p], timeout=3) |
| Uptime | psutil.boot_time() |
| Battery | psutil.sensors_battery() (laptops) |
Common problems and fixes
cpu_percent()always returns 0.0: you called it once without an interval. Passinterval=1, or call it once to prime and read it again later.psutil.AccessDenied: the process belongs to another user or the system. Catch the exception, or run with administrator or root rights if you really need it.psutil.NoSuchProcess: the process ended between listing and reading it. Catch it and move on; this is normal.- Process CPU above 100%: it is per core. Divide by
psutil.cpu_count()if you want a 0 to 100 scale for the whole machine. - Memory looks almost full on Linux: read
virtual_memory().availableor.percent, not.free. sensors_temperaturesmissing: temperature sensors are only exposed on Linux and FreeBSD; on Windows and macOS the function does not exist.
Where to go next
- To see where memory goes inside your own code rather than across the whole system, read Code Profiling for Memory Usage in Python.
- Turn the alert column from Step 5 into a real notification with Send Email with Python: smtplib, HTML and Attachments.
- Wrap the monitor in a polished command-line tool with live tables using Typer + Rich: Build Beautiful Modern CLIs in Python.
Conclusion
psutil turns system monitoring into plain Python: cpu_percent(), virtual_memory() and disk_usage() for machine health, process_iter() and Process for finding and controlling programs, and the I/O counters for live network and disk speed. With a loop and a CSV writer you already have a cross-platform monitor, and it is a solid building block for automation scripts in 2026.