What's Eating My Swap? Introducing `swap_report.py`!

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!

Introducing 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.

How It Works & Features

At its core, the script:

  • Iterates through the process IDs in /proc.
  • For each process, it reads the /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.
  • It filters out processes that aren't using any swap.
  • It then displays this information in a table, showing the PID, human-readable swap amount (kB, MiB, GiB), the user running the process, and the process command itself.
  • It also gives you a total swap usage for the listed processes.

Some of the key features include:

  • Sorting the output by swap usage (default), PID, or process name.
  • Filtering by a specific user.
  • Filtering by a process name using a regular expression.
  • Human-readable swap sizes (e.g., "4.03 MiB" instead of just kilobytes).
  • Optional color-coded output for high swap usage.
  • Ability to output the report to an HTML file for easier sharing or logging.

Here's a Peek:

This is what the output looks like in the terminal:

Run as me on one of the servers

Run as root to get all the details!

The Code:

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:

  1. Save the code above as swap_report.py.
  2. Make it executable: chmod +x swap_report.py
  3. Run it:
    • Basic report (top 30 processes by swap usage): ./swap_report.py
    • Show top 10 processes: ./swap_report.py -n 10
    • Sort by PID: ./swap_report.py -s pid
    • Filter by user "thecrazygm": ./swap_report.py -u thecrazygm
    • Filter for processes matching "python": ./swap_report.py -p python
    • Output to an HTML file: ./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



0
0
0.000
6 comments
avatar

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! 😁 🙏 💚 ✨ 🤙

0
0
0.000
avatar

You are welcome. Hearing things like that give me the warm fuzzies.

0
0
0.000
avatar

I'm happy to hear that. I tried it this morning, and it works great! I like the color coding for high-memory use. 😁 🙏 💚 ✨ 🤙

0
0
0.000