Our dictation app lagged by 282 seconds. The cause was a custom vocabulary list

Short answer: The lag came from a custom vocabulary hint. On silent or noisy audio, the Qwen3 speech model read our 30-word vocabulary list back, and real speech waited in a queue behind those echoes, up to 282 seconds. Version 2.0.50 adds the hint only when there is speech, detects echoes, and skips obsolete work; the slowest 5% of passes went from 271,781 ms to 346 ms the next day.

On the evening of September 27, we were sure the app had frozen. We would speak, and nothing would appear. Then, long after, text would show up in a burst.

Our first guess was load. We were building the app in the background, and the speech model shares the machine with everything else. That guess was wrong, and we had made the same wrong guess two days earlier. This is how the actual cause turned up, and why it is a good example of reading what the logs say before deciding what they mean.

A disclosure: Sorimac is our own product, a dictation app for Mac that runs speech recognition on the device. Everything below was measured on our test Mac.

What the logs showed

Sorimac transcribes in short passes while you speak. Each pass is logged with how long it took. In the 17:00 hour, 14 passes averaged 276 milliseconds. Normal.

From 18:14 on, short utterances of exactly 2.5 seconds started arriving one after another: 91 utterance starts between 18:14 and 18:31, 26 of them 2.5 seconds long. That length is a tell. It is 0.5 seconds of sound plus the 2.0 seconds of silence the app waits for before closing an utterance. It is the shortest thing a noise can open, not something a person says.

At 18:14:56 the first pass took 11 seconds. At 18:15:08, 19.5 seconds. By 18:30, 282 seconds. Across the 18:00 hour, 159 passes averaged a 61-second wait.

Bar chart of logged time per transcription pass on September 27: 0.276 seconds on average in the 17:00 hour, then 11 seconds at 18:14:56, 19.5 seconds at 18:15:08 and 282 seconds at 18:30

The output nobody spoke

The text those noise utterances produced was the giveaway. It was a 161-character string: "Sorimac, Pagong, Claude, Claude Code, Cursor, Codex, Qwen, vibe coding, ..." Our personal vocabulary list of 30 words, read back in order. It was logged 71 times that day.

None of it was typed into an app. A speech-rate filter caught it every time, because 161 characters cannot fit in half a second of sound. The damage was the time spent producing text that was going to be thrown away.

Looking back, the same output and the same 7 to 13 second passes appeared at 00:27 on September 25. We had blamed a build running in the background.

Three layers of cause

1. The echo. Custom vocabulary works as a hint: before decoding, the model is told "these words may appear". When there is speech, the hint nudges ambiguous sounds toward the right spelling. When there is no speech, there is nothing to nudge, and the Qwen3 model (the default engine in Sorimac) simply copies the hint.

We measured this directly:

Input Hint on Hint off
0.8 s of silence 116 characters, 1,267 ms 2 characters, 100 ms
0.8 s of white noise 116 characters, 1,420 ms 0 characters, 77 ms
Real speech (10.9 s clip) Same output both ways, no echo

2. A safety check that ran to the end. For input that looks like noise, the app decodes the same audio again at a higher temperature and checks whether the result wobbles. Noise usually produces different junk each time. The echo produced the identical list every time, which reads as "stable", so the check ran all six rounds.

3. Finishing work that was already obsolete. When a new utterance starts, the previous one's final transcript is going to be discarded anyway. The app only checked that after decoding had finished. The engine handles one job at a time, so real speech waited behind a queue of echoes.

The pass log made this hard to see at first: the time it recorded included the wait in the queue, not just the 0.3 seconds of actual decoding.

What changed in 2.0.50

Change How Why
Hints only when there is speech Add the vocabulary hint only if the audio's signal-to-noise ratio is 4 or higher. Real speech measured 51 to 94, noise alone 1.0 to 1.1 Removes the cause. Silent input never gets a hint to copy
Echo detection If the longest run shared between output and vocabulary covers 80% or more of the output (12+ characters), treat it as not a transcript and log it For leftovers, such as a loud click with a high ratio but no words
Skip obsolete work Drop passes and finals whose utterance has already been replaced, before decoding The result was discarded anyway. Same behavior, less waste

The threshold of 4 sits far from both sides, which is why we were comfortable picking a round number.

How it measured afterwards

Test Before After
0.8 s silence, hint setting on 1,267 ms, 116-char echo 114 ms, 2 chars
0.8 s white noise 1,420 ms, 116-char echo 89 ms, 0 chars
3.0 s white noise 1,765 ms, 144-char echo 193 ms, 2 chars
Real speech, 10.9 s 1,200 ms, 73 chars 985 ms, 73 chars, same sentence
Stream benchmark error rate, 6 files (normal / prefix forcing) 2.68% / 3.96% 2.39% / 3.07%

Read the last row carefully. Two runs before the fix differed from each other by about as much, because the benchmark simulates real time and small timing changes alter things like whether a number is written as digits. It shows the fix did not hurt accuracy. It does not show an improvement.

Real use the following day told the same story. The slowest 5% of transcription passes went from 271,781 ms on September 27 to 346 ms on September 28. The median time to the first typed character with the same engine went from 2,402 ms (September 25) to 1,327 ms (September 28). That second number is from only 50 utterances over about an hour, early in the morning, so we would not lean on it yet.

A related finding: hints can also hide speech

A week before, we had found the other face of the same mechanism on our V2.1 model, an optional engine we fine-tuned from Qwen3-ASR for Korean. With the 30-word hint on, short clips of real speech came back empty:

Clip Hint on Hint off
First 1 s 0 characters 32 characters
First 3 s 0 characters 18 characters
Full 11 s 35 characters, first 36 lost 71 characters, complete

On that model, a comma-separated list in the context suppressed output. In noise, that same suppression blocks ghosts, which is why it went unnoticed. Since then the app no longer injects vocabulary hints into V2.1 at all.

So a vocabulary hint is not a free accuracy boost. Depending on the model and the audio, it can make the model echo, or make it go quiet.

What we would tell another developer

What is still open: the noise that opens dictation in the first place. This fix removed the cost of a false start, not the false start. That became the next investigation, about keyboard clicks and why the detector opened on a single loud block.

FAQ

Why did the dictation app seem frozen?

It was not frozen. On silent and noisy audio, the Qwen3 model read our custom vocabulary list back, and real speech waited behind those discarded passes. By 18:30 on September 27, one pass took 282 seconds.

Can a custom vocabulary list make speech recognition worse?

Yes, depending on the model and the audio. On Qwen3 the hint was copied out on silence and noise. On our V2.1 model, a 30-word hint made short clips of real speech come back empty, so the app no longer adds vocabulary hints to V2.1.

What did version 2.0.50 change?

It adds the vocabulary hint only when the audio's signal-to-noise ratio is 4 or higher, treats output that mostly repeats the vocabulary as not a transcript, and drops work for utterances that were already replaced, before decoding.

Did the fix change accuracy?

Our stream benchmark error rate on 6 files went from 2.68% / 3.96% to 2.39% / 3.07%, but two runs before the fix differed by about as much. It shows the fix did not hurt accuracy, not that it improved it.