The fan was back.
If you've read the origin story, you know I have history with this sound. A month of an overheating MacBook, blamed on everything except the thing I should have checked. That whole story ended with me building Localhost Explorer so I'd never again have to open Terminal, run lsof, and squint at ports at the end of a long day.
So this time I did the thing I built the tool for. I clicked the menu bar icon.
And the app told me, very calmly, that everything was fine.
There was a listener I didn't recognize — qemu-system-aarch64-headless, a headless Android emulator I'd started for a test and, of course, never shut down. Localhost Explorer named it, told me where it came from, told me the safe way to stop it. Exactly its job. It sat in the list looking like every other row. A port. A PID. A tidy little profile.
Meanwhile the fan was at a full roar and the keyboard was hot under my hands.
I dropped to the shell to confirm what I already suspected:
ps -p 62456 -o %cpu,%mem,command
And there it was.
818% CPU.
Not a typo. On macOS, 100% is one full core, so a number can climb well past 100 when a process pins several at once. 818% is eight cores pinned by a background emulator I'd forgotten existed. That's the heat. That's the fan. That's the battery vanishing at lunch.
And my app — the one I built specifically so I'd stop having this exact problem — had shown it to me without a flicker of alarm.
The blind spot I'd shipped
Here's the uncomfortable part. This wasn't a bug. It was a blind spot I'd designed in from day one and never noticed.
Localhost Explorer was built to answer a question: what is listening on localhost, and what do I do about it? Name, port, PID, working folder, who manages it, the sane way to stop it. It answers that question well. But it's the question of identity — the who and where. It never once asked the other obvious question:
How hard is each of these things actually working?
The original tool would have shown you Ollama sitting quietly on 11434 and a runaway emulator melting your machine as two identical-looking rows. Same font. Same calm. The one piece of information that would have made me stop and look — this one is hot — was the one piece it didn't have.
A visibility tool that hides how hard things are working isn't finished. It just looks finished.
Adding the number — and the one line I refused to cross
Getting the data is not hard. ps will hand you %cpu and rss (resident memory) for any PID — the same tool I'd just run by hand, two more columns. The app already runs a scan; in principle you just ask for them.
Except the scan touches the most safety-critical code in the whole product, and I was not about to be casual near it.
When you click Stop, Localhost Explorer does something most naive process killers don't: it re-verifies the process's identity before it sends a single signal. PIDs get recycled — the number you saw a second ago can already belong to something else — so the app checks the process's start time against what it captured, and if it can't prove the PID is still the same process, it refuses. That guard is what keeps Stop from ever hitting the wrong process. (I wrote a whole post about why killing a process is harder than it looks.)
CPU and memory are, by comparison, decorative. Nice to see. Not something a stop decision should ever hang on.
So the rule I gave myself was simple: the decorative number is never allowed anywhere near the safety-critical parse.
Concretely, the CPU and memory figures come from a separate ps call, parsed on their own path. The identity check — the start-time comparison the Stop guard depends on — is left byte-for-byte untouched. If the resource call returns something garbled, or misses a PID entirely, the worst case is a row shows no number. A garbled reading has no way to reach the identity check — or the stop that depends on it. The telemetry is best-effort; the safety is not.
The same discipline applies to a detail that's easy to get wrong: I never clamp the CPU number to 100%. A tool that did would have shown my emulator as "100%" — technically a lie of omission, and exactly the wrong one. Eight cores pinned is categorically different from one, and the number has to say so. Activity Monitor shows you the same >100 figures for the same reason.
Making "hot" impossible to miss
A number in a column is easy to skim past. The whole failure was that I skimmed past a calm-looking row. So the signal had to be louder than a digit.
Two thresholds, chosen from what the numbers actually mean. Warm is half a core or more (50%) — worth a glance, drawn in amber. Hot is two full cores or more in a reading (200%) — usually a runaway, sometimes a heavy job you started on purpose, and either way worth a look — drawn in red, and bold.
Red and bold is deliberate, not decoration. Color alone fails the moment you're colorblind, or running the monochrome terminal theme, or piping the output somewhere plain — so the bold still reads as urgent when the color can't. In the terminal the same idea shows up as a plain-text mark: a ● for hot, a ▲ for warm, so even le list | grep ● surfaces the runaway with no color at all.
You see it in three places now, wherever you happen to be looking. There's a CPU column on every row, so a hot process announces itself in the list instead of waiting to be clicked. There's a CPU and memory line in the detail view, the hot figure carrying the same red-and-bold weight. And there's a Top CPU section in Stats — hottest first, so the worst offender is the first thing you read — which quietly isn't there when nothing's working hard. It only shows up when it has something to warn you about.
The free CLI got it too
The terminal version — le, the open-source one — got the exact same treatment in the same release. The live table grew a CPU column, le list prints the ●/▲ marks, and --json carries cpu and rss so you can wire "warn me when anything on localhost goes hot" into your own scripts.
The two share one contract on purpose: warm at 50%, hot at 200%, kept numerically identical in both codebases. The app and the CLI should never disagree about what "hot" means. If they drifted, one of them would be lying to you.
An error that fixes itself
While I was in there, I killed a rough edge the recycle guard had been leaving behind.
When that guard refuses a stop — the PID now belongs to a different process than the one you selected, so it says exactly that and asks you to refresh — it was correct, but it dead-ended. You'd get a sentence telling you to refresh, and then you had to go do it yourself, find the row again, and click Stop again. The tool knew exactly what needed to happen and made you do it by hand.
Now it just does it: a refused stop rescans on the spot and tells you it did, so the retry is one click on already-fresh data instead of a chore. A "try again" the tool can perform itself should perform itself.
What I actually learned
The first version of this story was about a command I knew and kept not running. This one is quieter and, honestly, more humbling: I'd built the tool to fix that, and I still shipped it half-blind. It answered the question I set out to answer so well that I never noticed it wasn't answering the one that would've saved me a month of fan noise all over again.
A tool is only as honest as the things it refuses to hide from you. "It's listening" was never the whole truth. "It's listening, and it's pinning eight cores" is.
The fan is quiet again. This time, the app is the reason I noticed.
See what's hot on your Mac.
$5. One Mac. Yours to keep. No tracking, no telemetry, no account. Or run the free CLI — le is open source.