Python psutil: CPU, Memory, Disk, and Network Usage
Learn how to read CPU, memory, disk, and network usage in Python with psutil. Includes code examples, return-value explanations, and common pitfalls.
Python's psutil library provides a consistent API for reading system resource usage, including CPU, memory, disk, and network metrics. This article shows how to read each metric, what the return values mean, and how to avoid common pitfalls when polling them in a real application.
Reading CPU Usage with psutil
The cpu_percent() function is the most direct way to get CPU utilization. With no arguments, it returns the percentage since the last call, which makes the first call meaningless in practice. To get a meaningful reading, pass an interval in seconds so psutil samples the CPU over that window.
import psutil # Block for 1 second and return the average CPU usage cpu = psutil.cpu_percent(interval=1) print(f"CPU usage: {cpu}%")
If you need non-blocking behavior, call cpu_percent(interval=None) once, then wait at least a short period before calling it again. The second call returns the usage since the first call. For a continuous monitor, you typically store the first result and discard it.
cpu_times() returns a named tuple with user, system, idle, and, depending on the operating system, fields such as iowait and irq. This is useful for calculating the percentage yourself over a time delta, which gives you more control than cpu_percent() when you are already sampling other metrics.
Monitoring Memory Usage
virtual_memory() returns a named tuple with total, available, percent, used, and free fields. available is the most useful figure for estimating how much memory can be allocated without swapping; on Linux it includes reclaimable page cache.
mem = psutil.virtual_memory() print(f"Total: {mem.total / (1024**3):.1f} GiB") print(f"Available: {mem.available / (1024**3):.1f} GiB") print(f"Used: {mem.used / (1024**3):.1f} GiB ({mem.percent}%)")
For swap memory, use swap_memory(). It returns total, used, free, percent, sin, and sout. The sin and sout fields count bytes swapped in and out, which is useful for detecting heavy swapping that can degrade performance.
Collecting Disk Usage and I/O Counters
disk_usage(path) returns a named tuple with total, used, free, and percent for the filesystem containing path. The path must exist; otherwise psutil raises FileNotFoundError.
usage = psutil.disk_usage('/') print(f"Root disk usage: {usage.percent}%")
disk_io_counters() returns a named tuple with read_bytes, write_bytes, read_count, write_count, and other fields. On Linux, this aggregates all disks by default. To get per-disk counters, pass perdisk=True to receive a dictionary keyed by disk name.
io = psutil.disk_io_counters() if io: print(f"Read: {io.read_bytes / (1024**3):.2f} GiB") print(f"Write: {io.write_bytes / (1024**3):.2f} GiB")
Note that disk_io_counters() may return None on some systems or when the OS does not expose the required kernel counters. Always check for None before accessing fields.
Tracking Network I/O and Connections
net_io_counters() returns a named tuple with bytes_sent, bytes_recv, packets_sent, packets_recv, and errin/errout. This gives cumulative totals since boot, so you typically compute deltas between samples to get a rate.
import time def network_rate(interval=1): before = psutil.net_io_counters() time.sleep(interval) after = psutil.net_io_counters() sent_rate = (after.bytes_sent - before.bytes_sent) / interval recv_rate = (after.bytes_recv - before.bytes_recv) / interval return sent_rate, recv_rate
For active connections, net_connections(kind='inet') returns a list of connection objects with fd, family, type, laddr, raddr, status, and pid. Depending on the platform and the users that own the processes, this may require elevated privileges. If access is denied, psutil raises psutil.AccessDenied, so catch that exception rather than assuming a generic PermissionError.
Handling Errors and Cross-Platform Differences
psutil abstracts most OS differences, but not all functions behave identically on every platform. For example, cpu_times() may include an iowait field on Linux, while macOS and Windows expose different fields. net_connections() also has platform-specific permission requirements. Wrap calls that may fail in try/except and catch psutil.Error subclasses such as AccessDenied and NoSuchProcess, as well as standard filesystem errors such as FileNotFoundError.
try: conns = psutil.net_connections(kind='inet') except (psutil.AccessDenied, psutil.NoSuchProcess) as e: print(f"Cannot list connections: {e}")
Also be aware that some functions return None when the underlying OS does not provide data, such as disk_io_counters() on certain virtualized systems. Check for None before using the result.
Performance Considerations When Polling Metrics
Calling cpu_percent(interval=1) blocks the current thread for one second. In a monitoring loop that also reads memory and disk, this serializes sampling and can make the loop slower than necessary. A better approach is to call cpu_percent(interval=None) once to establish a baseline and then call it again on each polling pass. The function returns the CPU percentage since the previous call without blocking.
import time import psutil # Establish a baseline; the first value is not meaningful. psutil.cpu_percent(interval=None) time.sleep(1) for _ in range(10): cpu = psutil.cpu_percent(interval=None) mem = psutil.virtual_memory() print(f"CPU: {cpu:.1f}% Memory: {mem.percent:.1f}%") time.sleep(1)
With this pattern, the CPU reading no longer forces a fixed delay inside the psutil call, so you can combine it with memory, disk, and network samples on your own schedule. For network and disk I/O, always compute deltas from cumulative counters rather than relying on a single reading, because the raw values are monotonic since boot.
When building a long-running monitor, be mindful of the overhead of calling net_connections() frequently. Parsing the full connection table can be expensive on systems with many sockets. If you only need total bytes, stick with net_io_counters() and avoid net_connections() unless you specifically need per-process or per-socket details.