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.
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
- Run iotop first to confirm Python is your I/O hog
- Use strace to identify which file descriptors are being hammered
- Check Python's
iomodule—are you using buffered I/O? The default is buffered, which is okay for large reads, but terrible for many small writes - Profile with cProfile to see which functions call the most I/O operations
- 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 (
asynciowithaiofiles) for concurrent read/write - Monitor disk I/O in production with tools like
iostatordstat
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.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.