Blog

Stats vs iStat Menus: 1.9% vs 0.1% idle CPU

Stats and iStat Menus on one MacBook Pro, sampled by one command at the same instant, with my own app as the third column. Everything that came back, including what makes mine look worse.

1 August 202610 min readMac15,6 · M3 Pro · 5P+6E · 18 GBmacOS 26.5.2 (25F84)n=160 paired samples

I did not set out to write a benchmark. I set out to find out if I was fooling myself.

You build a thing whose entire pitch is that it costs nothing to run, and after a few months you cannot really tell any more whether that is true or whether you have just been looking at friendly numbers.

So I installed the two apps everybody actually uses, put all three on the same MacBook Pro, and sampled them with one command, in one window, at the same moment. This is everything that came back, including the parts I would rather not publish.

Resource cost

Menu bar showing, panel closed, nobody interacting with anything. This is the state these apps are in for almost every minute of their running life, so it is the state worth measuring.

Idle CPUmean, lower is better

AirStats0.046%
iStat Menus0.116%
Stats1.931%

Memoryresident

AirStats11 MB
iStat Menus64 MB
Stats145 MB

Energy impactn=60

AirStats0.04
Stats1.94

A 48× gap that tracks the CPU result exactly.

ProcessnCPU meanMedianp95MaxMemoryThreads
AirStats1600.046%0.00%0.10%0.10%11 MB5
iStat Menus (suite)1600.116%0.00%0.30%1.60%64 MB11
Stats 3.0.91601.931%1.80%3.40%4.20%145 MB15

The line in that table I keep coming back to is not the mean. It is that AirStats' single worst sample of the entire run, 0.10%, sits below Stats' median of 1.80%. The worst moment of one app is eighteen times better than the typical moment of the other, which is not a tuning difference.

But light means nothing if the numbers are wrong

This is the part I actually cared about. An app can be cheap because it is careful, or cheap because it is caching a number from four minutes ago and telling you it is live. Those look identical in Activity Monitor.

So every collector got probed against an independent kernel source, sampled back to back so the two readings describe nearly the same instant. Every check but one reproduced exactly.

MetricGround truthResult
Wired memoryvm_stat pages × 1638414 of 14 exact
Swap used and totalsysctl vm.swapusage14 of 14 exact
Load averagesysctl vm.loadavg14 of 14 exact
Network bytes since bootnetstat -ib -I en03.1 GB down, 6.0 GB up, exact
Disk bytes since bootioreg IOBlockStorageDriver416 GB and 160 GB, exact
Battery state and healthpmset -g batt, 5490 of 6249 mAh87.9%, arithmetic closes
Performance and efficiency coreshw.perflevel*.logicalcpu5P and 6E, correct
CPU die temperaturemean of its own 7 sensors73.9 °C, reproduces
CPU busy totaltop aggregate, n=201.1 points lowThe two never share an identical window and top charges its own overhead into the figure it prints, so a small negative bias is expected.

What holds that together is one rule the collectors are tested against:

Never return a fabricated or zeroed value to paper over a failed syscall, and never compute a rate from a zero elapsed time.

That rule exists because I broke it. The GPU collector now primes and discards its first read, because macOS reports utilisation since the last read by any process on the machine. Before that fix it reported 98% on a completely idle GPU, and it was right, for a question nobody asked.

So why is it this light?

Four decisions, all of them visible in the source, none of them clever.

Sampling follows the screen. Five activity levels. A collector nothing is displaying is never sampled, and it releases what it was holding. The process collector, by far the most expensive thing here, runs only while the panel is open. Screen asleep suspends the timer entirely. The full design is here.

Nothing allocates on the tick. Tick buffers get swapped rather than reassigned, MIBs resolve once instead of walking a name resolver every sample, and SMC request buffers live as long as the collector that owns them.

Charts are shapes, not Canvas. SwiftUI's Canvas brings up the Metal renderer, a fixed 93 MB allocation you never get back. Drawing charts as plain shapes avoids it, and that single choice is most of the memory gap in the table above.

Redraw is observed, not timed. The status item re-renders when its model changes, not on a schedule. Nothing new to show means no snapshot, no redraw, no wakeup.

Now the parts that do not flatter me

Every benchmark written by the person who built one of the entrants deserves suspicion, so here are the three places I would attack this one.

Warm memory. That 11 MB is a freshly launched instance whose panel was never opened. Once the panel, settings and colour picker have all been shown, AirStats settles near 70 MB and stays there. Against a warm AirStats, the 64 MB iStat Menus suite is roughly a tie. The 11 MB number is real, but it is a cold number.

I cannot fully explain the CPU gap. Stats reads processes by shelling out to /bin/ps every second, according to its own source, which would explain its floor completely. But that module was not running in the default install I measured. So the 42× gap is measured honestly and explained only partly, and I would rather say that than pretend I know.

Scope, not efficiency. iStat Menus ships weather, per sensor customisation, notification history and far more widget configuration than AirStats has. AirStats covers the core of what these apps do and keeps no history across launches. Some of that memory gap is not waste, it is features I did not build.

Method, so you can attack it properly

Sampling. All three apps were sampled by one top -l 2 -s 1 invocation per interval, so every app is measured over an identical window. The -l 2 is not decoration: the first sample of any top run reports CPU cumulative since launch rather than over the interval, and reading it would have flattered whichever app had been running longest. Probably mine.

Sample counts. n=160 per process for the three way run, n=192 for the AirStats and Stats run, n=60 for energy, n=14 and n=20 for the accuracy checks. An interim cut at n=58 gave the same figures to within 0.03 percentage points, so the ranking is stable.

Machine state. The Mac was under a sustained artificial load the whole time: six orphaned yes processes pegging six cores at a load average between 7.6 and 8.0. All three apps were hit equally so the comparison holds, but the absolute percentages describe a busy machine, not an idle one.

Setup. Stats and iStat Menus were installed through Homebrew. iStat Menus was measured only after its trial setup was finished and its menu bar items were enabled. Hardware: Mac15,6, M3 Pro, 5 performance and 6 efficiency cores, 18 GB, on macOS 26.5.2 (25F84).

Run it yourself. It is one command, it takes a few minutes, and if your numbers disagree with mine I would like to know.

AirStats

Sixteen metrics in your menu bar for 0.046% CPU. Free and open source.

Download for Mac

Why your GPU said 98% on an idle Mac