Our dictation app grew to 35 GB of RAM. The first fix was wrong.
GHZ Voice is a small Mac app. It sits in the menu bar, records while you hold a hotkey, and pastes cleaned-up text where your cursor is. In June 2026 it started using 28 GB of RAM after a few days in the tray. A week later, after we had shipped a fix, it reached 35 GB and froze the Mac.
This is the story of the fix that did not work, the one that did, and the method we now use for every memory problem.
The first diagnosis looked right
The obvious suspects were easy to find. The dashboard checked the microphone and accessibility permissions every four seconds, forever, even with the window hidden. Those checks called Objective-C APIs from Tauri worker threads that had no autorelease pool, so the objects they created were never freed.
We stopped the polling when the window is hidden, wrapped the calls in autoreleasepool, capped the debug log at 1 MB, and shipped version 0.1.15. It felt done. We had not measured anything.
Measuring instead of guessing
When the app climbed to 35 GB anyway, we stopped reading code and started reading the heap. macOS ships the tools for this:
MallocStackLogging=1 "/Applications/GHZ Voice.app/Contents/MacOS/ghz-voice-app-prod"
vmmap -summary <pid>
malloc_history <pid> -allBySize
leaks <pid>leaks found almost nothing: 4 KB of strings. That was the first useful clue. The memory was not leaked in the classic sense. It was still reachable, held by something alive.
vmmap showed 960 MB in large allocations, 77 blocks. malloc_history gave us the call stack behind them:
CoreAudio HALC_IOThread::IOWorkLoop
→ [GHZ Voice audio callback]
→ realloc (Vec::extend)The audio thread was still appending samples to buffers of 100 to 134 MB. That is more than 500 seconds of audio, past the 300-second cap the app enforces on a recording.
The real cause
GHZ Voice uses the cpal crate to read the microphone. To stop a recording, the code dropped the stream. On macOS 26.2, dropping a cpal::Stream does not always stop the CoreAudio callback. Streams from old dictations kept running on the audio thread and kept growing their buffers.
One detail made it worse. In automatic mode the app opens several microphones at once and keeps the one that hears you best. The machine we test on has four inputs (an audio interface, the Studio Display, a webcam and AirPods Max), so every dictation left up to three orphaned streams behind.
The fix
- Each stream shares an
activeflag with its callback. The callback returns immediately once the flag is false. - Stopping a recording sets the flag, pauses the stream, then drops it, before reading the samples.
- A
Dropimplementation sets the flag on any exit path, including a panic. - A hard cap of about 60 MB per capture buffer, so even an unknown bug cannot grow without limit.
This time we measured before shipping 0.1.16:
- 960 MB → 0
- large allocations alive after 5 dictations
- 1.14 GB → 25 MB
- total malloc
- +21 MB
- resident memory, bounded
The same week we wired automatic updates into the app, so the next fix would not need every user to reinstall by hand. That is its own story: shipping updates to a Mac app without the App Store.