Hey everyone,
Following up on yesterday's post about the server mayhem and adding swap space to help buffer against the OOM killer, I ran into a common question: "Okay, swap is being used, but what exactly is using it?"
While tools like top
or htop
can show overall swap usage, getting a clear, process-by-process breakdown specifically for swap isn't always straightforward. So, as is often the case, if I can't easily find the tool I want, I tend to build it!
swap_report.py
This little Python script, swap_report.py
, is designed to do one thing: scan through your running processes and show you which ones are using swap space, how much they're using, and provide a nice, clean report. It uses the rich
library for some pretty console output.
At its core, the script:
/proc
./proc/[pid]/smaps
file, which contains detailed memory mapping information, and sums up the lines starting with "Swap:" to get the total swap used by that process.Some of the key features include:
This is what the output looks like in the terminal:
Also available from this GitHub Gist
The script is designed to be run with uv run --script
, which handles the dependencies like rich
.
#!/usr/bin/env -S uv run --quiet --script
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "rich",
# ]
#
# ///
"""
swap_report.py - See how swap is being used by what processes
Python version by: Michael Garcia <[email protected]> 2025-06-04
"""
import argparse
import os
import pwd
import re
from collections import namedtuple
from datetime import datetime
from rich import box
from rich.console import Console
from rich.table import Table
from rich.text import Text
Process = namedtuple("Process", ["pid", "swap_kb", "user", "cmd"])
def human_readable(kb): #
if kb >= 1048576:
return f"{kb / 1048576:.2f} GiB"
elif kb >= 1024:
return f"{kb / 1024:.2f} MiB"
else:
return f"{kb} kB"
def get_processes(user_filter=None, name_filter=None): #
processes = []
for pid in filter(str.isdigit, os.listdir("/proc")): #
smaps_path = f"/proc/{pid}/smaps" #
if not os.path.exists(smaps_path): #
continue
try:
with open(smaps_path) as f: #
swap_kb = sum( #
int(line.split()[1]) for line in f if line.startswith("Swap:") #
)
if swap_kb == 0: #
continue
except Exception:
continue
try:
user = pwd.getpwuid(int(os.stat(f"/proc/{pid}").st_uid)).pw_name #
except Exception:
user = "?"
if user_filter and user != user_filter: #
continue
try:
with open(f"/proc/{pid}/cmdline", "rb") as f: #
cmd = f.read().replace(b"\0", b" ").decode().strip() #
if not cmd: #
with open(f"/proc/{pid}/comm") as f2: #
cmd = f2.read().strip() #
except Exception:
cmd = "?"
if name_filter and not re.search(name_filter, cmd): #
continue
processes.append(Process(pid, swap_kb, user, cmd)) #
return processes
def color_for_swap(kb): #
if kb >= 1048576: #
return "bold white on red" #
elif kb >= 102400: #
return "bold black on yellow" #
else:
return "" #
def main(): #
parser = argparse.ArgumentParser(description="Show swap usage by process.") #
parser.add_argument( #
"-n", "--num", type=int, default=30, help="Show top N processes (default: 30)"
)
parser.add_argument( #
"-s",
"--sort",
choices=["swap", "pid", "name"],
default="swap",
help="Sort by (swap, pid, name)",
)
parser.add_argument("-u", "--user", help="Filter by user") #
parser.add_argument("-p", "--pattern", help="Filter by process name (regex)") #
parser.add_argument("-o", "--output", help="Output to file") #
parser.add_argument("--no-color", action="store_true", help="Disable color output") #
args = parser.parse_args() #
procs = get_processes(user_filter=args.user, name_filter=args.pattern) #
if args.sort == "swap": #
procs.sort(key=lambda p: p.swap_kb, reverse=True) #
elif args.sort == "pid": #
procs.sort(key=lambda p: int(p.pid)) #
elif args.sort == "name": #
procs.sort(key=lambda p: p.cmd.lower()) #
console = Console(record=bool(args.output)) #
table = Table( #
title=f"Swap Usage Report - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", #
box=box.SIMPLE_HEAVY, #
)
table.add_column("PID", justify="right", style="cyan", no_wrap=True) #
table.add_column("Swap", justify="right", style="magenta") #
table.add_column("User", style="green") #
table.add_column("Process", style="white") #
total_swap = 0 #
for proc in procs[: args.num]: #
swap_str = human_readable(proc.swap_kb) #
row_style = color_for_swap(proc.swap_kb) if not args.no_color else "" #
table.add_row(str(proc.pid), swap_str, proc.user, proc.cmd, style=row_style) #
total_swap += proc.swap_kb #
console.print(table) #
summary = Text( #
f"Total swap used by listed processes: {human_readable(total_swap)}", #
style="bold", #
)
console.print(summary) #
if args.output: #
console.save_html(args.output) #
if __name__ == "__main__": #
main() #
How to Use It:
swap_report.py
.chmod +x swap_report.py
./swap_report.py
./swap_report.py -n 10
./swap_report.py -s pid
./swap_report.py -u thecrazygm
./swap_report.py -p python
./swap_report.py -o swap_usage.html
It's a simple but effective little utility for getting a better handle on what's going on with your system's swap space. Hope it's useful for others too!
As always,
Michael Garcia a.k.a. TheCrazyGM
The utility is simply astounding.
!PAKX
!PIMP
!PIZZA
View or trade
PAKX
tokens.Use !PAKX command if you hold enough balance to call for a @pakx vote on worthy posts! More details available on PAKX Blog.
Dude, this is fantastic! This is a tool that I'd use evety day! I can't wait to take it for a spin! I am so glad that I follow you! Thank you! 😁 🙏 💚 ✨ 🤙
You are welcome. Hearing things like that give me the warm fuzzies.
I'm happy to hear that. I tried it this morning, and it works great! I like the color coding for high-memory use. 😁 🙏 💚 ✨ 🤙
$PIZZA slices delivered:
@ecoinstant(1/20) tipped @thecrazygm
Come get MOONed!