iotap

See what a process is really doing with your files and your network.

iotap is a command-line tool for macOS and Linux. Give it a process ID or a name and it reports every read and write that process makes as it happens: the file or the remote address, how many bytes were asked for and how many moved, how long the call took, and whether it failed. When you stop, it sums the trace up per file and per endpoint.

sudo iotap curl
iotap: tracing 4242 (curl); press Ctrl-C to stop
TIME PID OP FD REQUESTED RESULT LATENCY TARGET
14:13:20.004541 4242 sendto 5 517 517 0.041 ms tcp 192.168.1.20:61000 -> 93.184.216.34:443
14:13:20.104625 4242 recvfrom 5 16384 4096 0.041 ms tcp 192.168.1.20:61000 -> 93.184.216.34:443
14:13:20.104708 4242 recvfrom 5 16384 EAGAIN 0.041 ms tcp 192.168.1.20:61000 -> 93.184.216.34:443
14:13:20.104791 4242 write 4 4096 4096 0.041 ms /Users/me/page.html
14:13:20.104875 4242 write 1 20 20 0.041 ms /dev/ttys004
iotap: 4242 (curl) exited

Read top to bottom, this is the whole life of one curl:

  1. Sends a 517-byte request to 93.184.216.34 on port 443.
  2. Asks for 16 KiB back and gets 4 KiB.
  3. Asks again and finds nothing waiting yet: EAGAIN.
  4. Writes the 4 KiB to page.html.
  5. Writes 20 bytes of progress to the terminal, then exits.

The usual tools each show a piece

Something on your machine is busy and you want to know with what. A process is chewing through the disk, an app you just installed is talking to somewhere you never asked for, a build step is slow and you suspect I/O, or a program keeps failing and you want to see which file or connection it trips on.

iotap puts the pieces together: the bytes, the process they belong to, and the file or endpoint they went to.

What each tool shows and what it leaves out
ToolShowsLeaves out
lsof, netstatWhat is open right nowWhat moves through it
Activity Monitor, topHow many bytesWhere they went
A packet captureThe traffic of the whole machineWhich process it belongs to
strace, fs_usageEach call a process makesThe adding up, per file and per endpoint
iotapEach read and write of the processes you name, with bytes, latency and target, summed per file and endpointMemory-mapped I/O, and on Linux io_uring and libaio

Choose what to watch

A target is a process ID or a name. A name matches every running process whose name, executable or first argument equals it, ignoring case, and iotap also traces processes started later under that name.

With -f, it also traces every process the traced ones start, those running when it begins and those started later.

sudo iotap 1234                # one process
sudo iotap Safari              # every Safari process, and new ones
sudo iotap -f make             # make and every process it starts
sudo iotap -i wlan0 firefox    # only network I/O over wlan0
sudo iotap -q -d 10 Finder     # summary only, after 10 seconds

One trace, four ways to read it

The event stream is what you saw above. When tracing ends, iotap sums it up. For a process that runs longer, the terminal UI shows the same tables live, JSON Lines feed jq or a log, and a recording lets you replay the trace later, in any of these forms, without root.

Summary

When you press Ctrl-C or the process exits, iotap sums the trace up per file and per endpoint. Here the network traffic went over en0, the interface that holds 192.168.1.20.

iotap summary: 4242 (curl) traced for 0.1 s
Files (2 targets)
READ CALLS WRITTEN CALLS FAILED TARGET
0 B 0 4.0 KiB 1 0 /Users/me/page.html
0 B 0 20 B 1 0 /dev/ttys004
Network (1 target)
RECEIVED CALLS SENT CALLS FAILED TARGET
4.0 KiB 2 517 B 1 1 tcp 93.184.216.34:443
Totals
files read 0 B (0 calls), written 4.0 KiB (2 calls)
network received 4.0 KiB (2 calls), sent 517 B (1 call)
en0 received 4.0 KiB (2 calls), sent 517 B (1 call)
5 calls, 1 failed

Terminal UI

--tui shows throughput for the last complete second and in total, then three tabs: files, network endpoints, and the latest 10,000 events. This is the Files tab after replaying a recorded download.

iotap 4242 curl (exited) 0:00:12
FILE READ FILE WRITTEN NET RECEIVED NET SENT
per second 0 B 1.2 MiB 1.2 MiB 0 B
total 0 B 14.1 MiB 14.1 MiB 517 B
End of the recording. Press q for the summary.
1 Files (2) 2 Network (1) 3 Events (7,205) sort: bytes
READ CALLS WRITTEN CALLS FAILED IDLE TARGET
0 B 0 14.1 MiB 3601 0 <1s /Users/me/big.iso
0 B 0 20 B 1 0 <1s /dev/ttys004
4242 (curl) exited q quit s sort p pause i interfaces r reset ↑↓ select enter details
  • 1 2 3 switch tabs
  • s sort by bytes, read, write, calls or recency
  • Enter details of the selected row
  • p pause the view; tracing goes on
  • y copy the selected path or address
  • i traffic of each network interface
  • q quit and print the summary

JSON Lines

--json writes every event as one line, with times in nanoseconds since the Unix epoch and the target as a small object. Pipe it to jq, or keep it as a log. This is one event, spread over several lines.

{
"type": "event",
"time_ns": 1790259200104708333,
"pid": 4242,
"tid": 2,
"op": "recvfrom",
"dir": "read",
"syscall": "recvfrom",
"fd": 5,
"requested": 16384,
"bytes": null,
"messages": null,
"errno": 35,
"error": "EAGAIN",
"latency_ns": 41666,
"target": {
"kind": "socket",
"proto": "tcp",
"local": "192.168.1.20:61000",
"remote": "93.184.216.34:443"
},
"interface": "en0",
"resolved": "traced"
}
sudo iotap --json curl | jq -c 'select(.type == "event")'

Recordings

--record FILE saves the trace, and --replay FILE feeds it through the same processing, so a replay reproduces the output of the live run in any output mode. A recording holds paths and addresses but no transferred data, and replays on the operating system it was made on.

sudo iotap --net-only --record t.iotaprec 1234
iotap --replay t.iotaprec              # the same output again, without root

It reads what the kernel already records

Every read, write, open and connect a program makes is a system call, and the kernel can be told to log each one as it happens, with the process it came from, its arguments and its result. iotap reads that log.

It attaches no debugger, injects no code and changes nothing in the program it watches.

On macOS
The log is the kernel trace facility, kdebug, the same source Apple's fs_usage reads. It is built into the kernel, so nothing has to be installed.
On Linux
iotap loads a small eBPF program of its own onto the kernel's syscall tracepoints. The kernel verifies it before running it and unloads it when iotap exits.
Why it needs sudo
Both are a view into every process on the machine, so the kernel gives them only to root. Without it, iotap says so and stops before it touches anything. Replaying a recording needs no root.
Metadata only
iotap records the call, the descriptor, byte counts, latency, the path and the socket endpoint. It never reads or stores the data being transferred: a process reading your SSH key shows up as a read of that file and its size, never its contents.
Only what you name
The kernel is asked to record the calls of the processes you name and, with -f, of the processes they start. No others.
Nothing left behind
On macOS only one program can own kdebug at a time, so iotap releases it on every exit path, including errors, signals and panics. On Linux the kernel drops the eBPF program the moment iotap exits. The terminal UI restores the terminal the same way.
One binary, no daemon
iotap runs only while you run it and makes no connections of its own. The optional host name lookups (--resolve) go through the system's resolver.
What it does not see
Memory-mapped file I/O and I/O the kernel does on a process's behalf, such as page-cache writeback. On Linux also I/O submitted through io_uring or libaio. The Limitations section of the README lists the rest.

Build it, then run it with sudo

iotap builds from source with Cargo. It runs on macOS, and on Linux 5.8 or later on 64-bit Arm or x86-64 with BPF and syscall tracepoints, as distribution kernels have them.

To build you need Rust 1.98.1, which rust-toolchain.toml selects. On macOS, add the Xcode Command Line Tools. On Linux, add clang, make, pkg-config, a C compiler, and the libelf and zlib development files.

On Debian and Ubuntu, install what the Linux build needs first:

sudo apt install build-essential clang pkg-config \
  libelf-dev zlib1g-dev

Then build it and trace a process:

git clone https://github.com/rath/iotap.git
cd iotap
cargo build --release
sudo ./target/release/iotap curl