I Asked My Terminal Which App Was Eating My Data — So I Built It
Posted on July 23, 2026 by admin — 6 min
I'm on a capped 5G home router. Go over the limit and things get throttled hard. So I actually cared, for once, about a question I used to only ask when a laptop fan spun up: which app is eating my data, and when did it spike?
The router could tell me the total. Activity Monitor could tell me the live rate. Neither could tell me the thing I actually wanted to know — a history. bandwhich, iftop, nettop itself, even iStat Menus: all of them are built for right now. Close the terminal, lose the answer. None of them remember.
So I built the tool that was missing, in one afternoon, and shipped it.
Starting stupidly small
The first version wasn't a tool, it was a one-liner. nettop -P printing rows in a loop, piped through awk to squint at. Then a bash script wrapped around it, then a genuinely ugly bash TUI with tput cursor codes, because I wanted something to stare at while I figured out what I actually needed.
That's usually where side projects like this die — as a script you run twice and forget. This one didn't, because somewhere in that first hour I hit the idea that reframed the whole thing.
The insight: throttling speed doesn't cut usage
Knowing "my connection is currently doing 4MB/s" doesn't save you a single megabyte. What saves you is knowing which app did it and when — so you can go kill the offender, or just understand your own habits well enough to stop hitting the cap. The product wasn't a live meter. It was memory. History-per-app, plus something that taps you on the shoulder when a process suddenly starts eating way more than its baseline.
Once I saw it that way, the feature list wrote itself: datasip apps --since 24h for a ranked leaderboard, datasip app chrome --since 8h to interrogate one process, datasip spikes --since 24h to see exactly when things went sideways.
A few design decisions worth explaining
Per-minute JSONL buckets. Instead of a database or a fixed ring buffer, samples land in tiny per-minute JSON-lines files under ~/.netwatch/. It's the least clever storage format possible, and that's the point — it's trivially appendable, trivially queryable over any --since window from 5m up to 30d, and if something ever goes wrong I can just cat a file and read it like a human.
An EMA baseline for spike detection. "Spike" only means anything relative to what's normal for that app. An exponential moving average per process gives a rolling baseline that adapts as usage patterns shift, so a video call app that's always heavy doesn't trip the same alarm threshold as a note-taking app that suddenly starts uploading megabytes it has no business uploading.
A single-writer lock. There are two things that need to touch this data at once: a background logger (datasip log, running under a launchd agent) constantly sampling and appending, and a live dashboard you might open at any moment to look at what's happening right now. Only one of them should ever be writing. A simple lock file made that coexistence boring, which is exactly what you want from infrastructure you didn't mean to build.
The rewrite nobody asked for
I prototyped the whole thing in Python first, because that's where my head goes for anything involving strings, files, and quick iteration. But the moment it worked, the delivery problem became obvious: a Python tool means "make sure you have Python 3.x, and probably a venv, and pip install these three things." That's a real tax on anyone who just wants to see which app ate their data.
So I ported it to Go, rebuilt the TUI on Bubble Tea and Lipgloss, and now the whole thing ships as a single static binary. brew install zainulabidin302/tap/datasip (or go install github.com/zainulabidin302/datasip@latest, or clone-and-go build if you want to read the code first) and you're done. No runtime, no interpreter version to match, no venv to explain to future-me in six months.
Shipping mechanics (the unglamorous part)
MIT license, pushed to GitHub, a Homebrew tap that builds from source, a GitHub Actions CI workflow so a broken build doesn't slip through. All fairly mechanical.
The part that actually cost time: the demo GIF. I used vhs to script a reproducible terminal recording, and the recording kept failing in ways that had nothing to do with the tool itself — PATH issues inside the vhs container meant it couldn't find the binary it was supposed to be recording. I'd fix the tape script, rerun it, watch it fail slightly differently. It's a good reminder that the GIF is the marketing for a CLI tool — nobody reads a terminal app's feature list before they've seen it move — so it's worth fighting for even when the fight is this dumb.
What I'd want a technical reader to take from this
You don't need root, a kernel extension, or a packet-capture library to do useful per-app network accounting on macOS — nettop and netstat are already there, already sampling at the OS level, and just need someone to remember what they said a minute ago. The "remember over time" gap is a real, useful wedge to build in even in a space that looks crowded on the surface — bandwhich and iftop are good tools, they're just answering a different question than the one I had. And the Python-to-Go rewrite wasn't about performance, it was about distribution: a single binary you can brew install is a genuinely different adoption story than "clone this and set up a virtualenv."
Honest limits
It's macOS-only for now — nettop/netstat on Linux behave differently enough that porting is real work, not a flag flip. Per-app resolution bottoms out at one minute, so it won't tell you about a five-second spike. And it tracks this Mac's usage, not the whole household sitting behind the same router — if your data cap problem is actually your partner's 4K streaming habit, datasip on your laptop won't see it.
Try it, break it, tell me what's wrong with the spike heuristic
brew install zainulabidin302/tap/datasip
datasip apps --since 24h
Repo's at github.com/zainulabidin302/datasip. If you've got opinions on the EMA spike threshold — too twitchy, not twitchy enough — that's exactly the kind of feedback I want. And if you're on Linux and annoyed it doesn't work there yet: that's flagged as an open, genuinely good first issue.