FAQ¶
This section answers common questions and pitfalls when using psutil.
General¶
Why should I avoid positional unpacking of named tuples?¶
Most psutil functions return named tuples. It is tempting to unpack them
positionally, but field order may change across major releases (as happened
in 8.0 with cpu_times() and Process.memory_info()). Always use
attribute access instead:
# bad
rss, vms = p.memory_info()
# good
m = p.memory_info()
print(m.rss, m.vms)
See the migration guide for the full list of field-order changes in 8.0.
Exceptions¶
Why do I get AccessDenied?¶
AccessDenied is raised when the OS refuses to return information about a
process because the calling user does not have sufficient privileges. This is
expected behavior and is not a bug. It typically happens when:
querying processes owned by other users (e.g. root)
calling certain methods like
Process.memory_maps(),Process.open_files()orProcess.net_connections()for privileged processes
You have two options to deal with it.
Option 1: call the method directly and catch the exception:
import psutil p = psutil.Process(pid) try: print(p.memory_maps()) except (psutil.AccessDenied, psutil.NoSuchProcess): pass
Option 2: use
process_iter()with a list of attribute names to pre-fetch. BothAccessDeniedandNoSuchProcessare handled internally: the corresponding method returnsNone(orad_value) instead of raising. This also avoids the race condition where a process disappears between iteration and method call:import psutil for p in psutil.process_iter(["name", "username"], ad_value="N/A"): print(p.name(), p.username()) # no try/except needed
Why do I get NoSuchProcess?¶
NoSuchProcess is raised when a process no longer exists. The most common
cause is a TOCTOU (time-of-check / time-of-use) race condition: a process can
die between the moment its PID is obtained and the moment it is queried. The
following two naive patterns are racy:
import psutil
for pid in psutil.pids():
p = psutil.Process(pid) # may raise NoSuchProcess
print(p.name()) # may raise NoSuchProcess
import psutil
if psutil.pid_exists(pid):
p = psutil.Process(pid) # may raise NoSuchProcess
print(p.name()) # may raise NoSuchProcess
The correct approach is to use process_iter(), which handles
NoSuchProcess internally and skips processes that disappear during
iteration:
import psutil
for p in psutil.process_iter(["name"]):
print(p.name())
If you have a specific PID (e.g. a known child process), wrap the call in a try/except:
import psutil
try:
p = psutil.Process(pid)
print(p.name(), p.status())
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
An even simpler pattern is to catch Error, which implies both
AccessDenied and NoSuchProcess:
import psutil
try:
p = psutil.Process(pid)
print(p.name(), p.status())
except psutil.Error:
pass
Processes¶
PID reuse¶
Operating systems recycle PIDs. A Process object obtained now may
later refer to a different process if the original one terminated and a new one
was assigned the same PID.
How psutil handles this:
Most read-only methods (e.g.
Process.name(),Process.cpu_percent()) do not check for PID reuse and instead query whatever process currently holds that PID.Signal methods (e.g.
Process.send_signal(),Process.suspend(),Process.resume(),Process.terminate(),Process.kill()) do check for PID reuse (via PID + creation time) before acting, raisingNoSuchProcessif the PID was recycled. This prevents accidentally killing the wrong process (BPO-6973).Set methods
Process.nice()(set),Process.ionice()(set),Process.cpu_affinity()(set), andProcess.rlimit()(set) also perform this check before applying changes.
Process.is_running() is the recommended way to verify whether a
Process instance still refers to the same process. It compares PID and
creation time, and returns False if the PID was reused. Prefer it over
pid_exists().
What is a zombie process?¶
A zombie process is a process that has finished execution but whose
entry remains in the process table until the parent calls wait(). When
psutil encounters a zombie process it raises ZombieProcess, a
subclass of NoSuchProcess.
What you can and cannot do with a zombie:
A zombie process can be instantiated via
Process(pid) without error.Process.status()always returnsSTATUS_ZOMBIE.Process.is_running()andpid_exists()returnTrue.The zombie appears in
process_iter()andpids().Sending signals (
Process.terminate(),Process.kill(), etc.) has no effect.Most methods (
Process.cmdline(),Process.exe(),Process.memory_maps(), etc.) may raiseZombieProcess, return a meaningful value, or return a null/empty value depending on the platform.Process.as_dict()will not crash.
How to create a zombie:
import os, time
pid = os.fork() # the zombie
if pid == 0:
os._exit(0) # child exits immediately
else:
time.sleep(1000) # parent does NOT call wait()
How to detect zombies:
import psutil
for p in psutil.process_iter(["status"]):
if p.status() == psutil.STATUS_ZOMBIE:
print(f"zombie: pid={p.pid}")
How to get rid of a zombie:
The only way is to have its parent process call wait() (or waitpid()).
If the parent never does this, killing the parent will cause the zombie to be
re-parented to init / systemd, which will reap it automatically.
Why does open_files() not return all files on Windows?¶
Process.open_files() on Windows is not guaranteed to enumerate all
regular file handles. The underlying Windows API may hang when retrieving
certain handle names, so psutil spawns a thread to query each handle
and kills it if it doesn’t respond within 100 ms. This means some entries can
be missed. This is a known OS-level limitation shared by tools like Process
Hacker (see issue 597).
What is the difference between pid_exists() and Process.is_running()?¶
pid_exists() checks whether a PID is present in the process list.
Process.is_running() does the same, but also detects
PID reuse by comparing the process creation time. Use
pid_exists() when you have a bare PID and don’t need to guard against
reuse (it’s faster). Use Process.is_running() when you hold a
Process object and want to confirm it still refers to the same
process.
CPU¶
Why does cpu_percent() return 0.0 on first call?¶
cpu_percent() (and Process.cpu_percent()) measures CPU usage
between two calls. The very first call has no prior sample to compare
against, so it always returns 0.0. The fix is to call it once to initialize
the baseline, discard the result, then call it again after a short sleep:
import time
import psutil
psutil.cpu_percent() # discard first call
time.sleep(0.5)
print(psutil.cpu_percent()) # meaningful value
Alternatively, pass interval to make it block internally:
print(psutil.cpu_percent(interval=0.5))
The same applies to Process.cpu_percent():
p = psutil.Process()
p.cpu_percent() # discard
time.sleep(0.5)
print(p.cpu_percent()) # meaningful value
Can Process.cpu_percent() return a value higher than 100%?¶
Yes. On a multi-core system a process can run threads on several CPUs at the
same time. The maximum value is psutil.cpu_count() * 100. For example, on a
4-core machine a fully-loaded process can reach 400%. The system-wide
cpu_percent() (without a Process) always stays in the 0–100%
range because it averages across all cores.
The returned value is explicitly not split evenly between all available CPUs.
This is consistent with the top UNIX utility: a busy loop on a system with
2 logical CPUs is reported as 100%, not 50%. Note that
Windows taskmgr.exe behaves differently (it would report 50%). To emulate
that: p.cpu_percent() / psutil.cpu_count().
What is the difference between psutil, os, and multiprocessing cpu_count()?¶
os.cpu_count()returns the number of logical CPUs (including hyperthreads). It is the same aspsutil.cpu_count(logical=True), but psutil does not honourPYTHON_CPU_COUNTenvironment variable introduced in Python 3.13.os.process_cpu_count()(Python 3.13+) returns the number of CPUs the calling process is allowed to use (respects CPU affinity and cgroups). The psutil equivalent islen(psutil.Process().cpu_affinity()).multiprocessing.cpu_count()returns the same value asos.process_cpu_count()(Python 3.13+).psutil.cpu_count()withlogical=Falsereturns the number of physical cores, which has no stdlib equivalent.
Memory¶
What is the difference between virtual_memory() available and free?¶
virtual_memory() returns both free and available, but
they measure different things:
free: memory that is not being used at all.available: how much memory can be given to processes without swapping. This includes reclaimable caches and buffers that the OS can reclaim under pressure.
In practice, available is almost always the metric you want when
monitoring memory. free can be misleadingly low on systems where the
OS aggressively uses RAM for caches (which is normal and healthy). On Windows,
free and available are the same value.
What is the difference between RSS and VMS?¶
rss(Resident Set Size) is the amount of physical memory (RAM) currently mapped into the process.vms(Virtual Memory Size) is the total virtual address space of the process, including memory that has been swapped out, shared libraries, and memory-mapped files.
rss is the go-to metric for answering “how much RAM is this process
using?”. Note that it includes shared memory, so it may overestimate
actual usage when compared across processes. vms is generally larger
and can be misleadingly high, as it includes memory that is not resident in
physical RAM. Both values are portable across platforms and are returned by
Process.memory_info().
When should I use memory_footprint() vs memory_info()?¶
Process.memory_info() returns rss
(Resident Set Size), which includes
shared libraries counted in every process that uses
them. For example, if libc uses 2 MB and 100 processes map it, each process
includes those 2 MB in its rss.
Process.memory_footprint() returns uss
(Unique Set Size), i.e. private memory of the process. It
represents the amount of memory that would be freed if the process were
terminated right now. It is more accurate than RSS, but substantially
slower and requires higher privileges. On Linux it also returns pss
(Proportional Set Size) and swap.
Why does virtual_memory() used + free != total?¶
Because some memory (like page cache and buffers) is reclaimable and accounted separately:
>>> import psutil
>>> m = psutil.virtual_memory()
>>> m.used + m.free == m.total
False
The available field already includes this reclaimable memory and is
the best indicator of memory pressure. See What is the difference between virtual_memory() available and free?.