How-tos

Profile Python I/O Bottlenecks with iotop

Learn how to use iotop to identify disk I/O bottlenecks in Python scripts, with real-world examples and practical fixes for speeding up slow file operations.

August 2026 8 min read 10 views 0 hearts

Profiling I/O Bottlenecks in Python with iotop

Ever stared at a Python script that's taking ages to finish, and you're just not sure why? You've checked CPU usage—it's low. Memory? Fine. But your disk is churning like it's grinding coffee beans.

That's your I/O bottleneck screaming for attention. And when Python hits the disk frequently (reading files, writing logs, saving data), it can slow everything down without raising obvious flags. CPUs are fast. Disks are not. So if your script spends more time waiting for reads and writes than actually computing, you've got a problem.

What Exactly is an I/O Bottleneck?

An I/O bottleneck happens when your program spends most of its time waiting for input/output operations to complete. In Python, this often comes from:

  • Reading or writing large files line by line
  • Frequent database queries (especially with SQLite)
  • Logging to disk too aggressively
  • Serializing/deserializing big objects (pickle, JSON)
  • File system operations in tight loops

The tricky part? Python's global interpreter lock (GIL) doesn't help here. While I/O operations release the GIL, the disk is still slow. And if you're doing async or multiprocessing without thinking about disk contention, you might actually make things worse.

Why iotop is Your Friend

iotop is a Linux utility that shows you which processes are using disk I/O in real time. Unlike top or htop (which focus on CPU/memory), iotop tells you exactly how much read/write throughput each process is generating.

You can't profile what you can't see. And if you're near max disk throughput, every extra I/O your Python script does will add latency.

Installing iotop

On most Linux distros:

sudo apt-get install iotop   # Debian/Ubuntu
sudo yum install iotop       # RHEL/CentOS

You'll need root or CAP_NET_ADMIN permissions.

Profiling a Python Script Step by Step

Let's say you have a script called data_pipeline.py that processes a bunch of CSV files. It feels slow, but you're not sure why.

Step 1: Run iotop in Batch Mode

Instead of the interactive UI, use batch mode to capture data over time:

sudo iotop -o -b -d 1 -n 30 > iotop_output.txt
  • -o: only show processes doing I/O
  • -b: batch mode (no interactive interface)
  • -d 1: sample every second
  • -n 30: collect 30 samples

Step 2: Run Your Python Script

In another terminal:

python data_pipeline.py &

Check its PID: echo $!

Step 3: Analyze the Output

Open iotop_output.txt. You'll see lines like:

TOTAL DISK READ: 120.45 M/s | TOTAL DISK WRITE: 87.23 M/s
python3   PID 1234  DISK READ: 85.23 M/s  DISK WRITE: 12.01 M/s

If your Python process is using most of the total disk throughput, you've found the bottleneck.

Interpreting iotop Results for Python

Here's the real-world translation:

  • High DISK READ: Your script is reading lots of data from disk. Maybe you're loading entire CSV files into memory when you don't need to.
  • High DISK WRITE: You're writing more than needed. Could be too-frequent logging, intermediate file creation, or redundant saves.
  • Both high: Classic heavy data pipeline or ETL job.

If iotop shows that other processes (like database daemons) are also consuming I/O, your Python script might be competing with them.

Real Example: A Logging Nightmare

A developer at PythonSkillset once had a script that processed 10 million records. It took 3+ hours. They couldn't figure out why. CPU was at 15%, memory at 30%.

They ran iotop and saw:

python3   PID 5678  DISK WRITE: 45.67 M/s

Turns out, every processing step was writing a log line to a file. 10 million log entries == 10 million disk writes. Just refactored to batch logs every 1000 entries, and runtime dropped to 45 minutes.

The fix:

# Before: writing every iteration
for record in records:
    process(record)
    log(f"Processed record {record.id}")

# After: batch logging
log_buffer = []
for i, record in enumerate(records):
    process(record)
    log_buffer.append(f"Processed record {record.id}")
    if i % 1000 == 0:
        with open("log.txt", "a") as f:
            f.write("\n".join(log_buffer))
        log_buffer = []
# Don't forget the last batch
if log_buffer:
    with open("log.txt", "a") as f:
        f.write("\n".join(log_buffer))

Common I/O Bottleneck Patterns in Python

Pattern What it looks like in iotop Fix
Reading files one line at a time High DISK READ, low CPU Use readlines() or pandas.read_csv() in chunks
Frequent small writes High DISK WRITE, process doing many I/O calls Buffer writes, use io.StringIO
Database queries in a loop High DISK READ (from db files) Use batch queries, add indexes
Pickle/JSON serialization of large objects High DISK WRITE bursts Use faster formats like Parquet or Feather
Logging too verbosely Constant DISK WRITE Adjust log levels, use buffered logging

When Not to Trust iotop Alone

iotop shows which process is doing I/O, but not why inside the process. It won't tell you if the bottleneck is from file reading, database access, or something else. For that, you need deeper profiling with tools like strace or Python's cProfile.

Example:

strace -e trace=read,write -p <PID>

This shows each syscall your Python process makes. If you see thousands of read() calls with small buffer sizes, that's your problem.

Practical Workflow for Python I/O Debugging

  1. Run iotop first to confirm Python is your I/O hog
  2. Use strace to identify which file descriptors are being hammered
  3. Check Python's io module—are you using buffered I/O? The default is buffered, which is okay for large reads, but terrible for many small writes
  4. Profile with cProfile to see which functions call the most I/O operations
  5. Fix the bottleneck (buffer, batch, or use faster storage)

Preventative Measures

  • Use with open() context managers (they flush buffers properly)
  • For large files, use memory-mapped files (mmap) or streaming parsers
  • Consider asynchronous I/O (asyncio with aiofiles) for concurrent read/write
  • Monitor disk I/O in production with tools like iostat or dstat

Wrapping Up

iotop is one of those hidden gems that every Python developer should have in their toolkit. It gives you instant visibility into disk I/O usage per process. When your script feels sluggish for no apparent reason, don't just stare at CPU or memory—run iotop and see if your disk is screaming for mercy.

I/O bottlenecks are often the easiest to fix once you see them. Because you can't optimize what you can't measure. And with iotop, you're measuring exactly what matters.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.