← writingJul 2026 · 2 min · CLI · perf

bunkill: making node_modules cleanup 2× faster on Bun

npkill is the tool I reach for to reclaim disk, but on a laptop with a few hundred repos it takes its time. bunkill is my re-take on the same idea, built on Bun, and the goal was simple: same UX, roughly half the wall-clock.

The speedup wasn’t clever, it was structural

The obvious win is concurrency — but the real one was not re-walking finished trees. Once a directory contains a node_modules, there is nothing useful below it, so the scan stops descending there:

async function* walk(dir: string): AsyncGenerator<string> {
  const entries = await readdir(dir, { withFileTypes: true });
  const hit = entries.find((e) => e.isDirectory() && e.name === "node_modules");
  if (hit) { yield join(dir, "node_modules"); return; } // prune — don't recurse in
  await Promise.all(
    entries.filter((e) => e.isDirectory() && !e.name.startsWith("."))
           .map((e) => drain(walk(join(dir, e.name)))),
  );
}

That single return prunes the deepest, densest part of the tree. Pair it with Bun’s fast readdir/stat and a bounded worker pool, and a full $HOME scan drops from ~40s to under 20s on my machine.

The rest is UX

Interactive fuzzy search, sort by size (so the 3 GB node_modules you forgot about in 2023 floats to the top), and a dry-run so you can see what you’re about to delete. It ships as an npm package — bunx bunkill and you never install anything.

The lesson I keep relearning with CLIs: the fastest code is the code you convince yourself not to run.