Pit record

PortAudio stream stop()/close() on a hotkey-callback thread can freeze the global hotkey listener

A push-to-talk daemon can look hung (process alive, hotkey dead) because PortAudio stop()/close() was called inline in the pynput key-release callback; on a flaky device that blocks and wedges the single listener thread. Move audio teardown and processing to a background thread and keep the callback non-blocking.

Fast answer

Problem: a push-to-talk dictation daemon intermittently goes unresponsive after switching windows and dictating

Root cause: stop()/close() of a PortAudio stream on a flaky/display-connected device can block for a long time.

Fix first: Move recorder.stop() (the PortAudio stream.stop()/close() calls) AND all downstream processing off the pynput key-release callback thread into a background thread.

Verify: Use the daemon heavily across multiple windows after the change.

Queries this answers

  • PortAudio stream stop()/close() on a hotkey-callback thread can freeze the global hotkey listener
  • PortAudio stream stop()/close() on a hotkey-callback thread can freeze the global hotkey listener fix
  • PortAudio stream stop()/close() on a hotkey-callback thread can freeze the global hotkey listener root cause
  • how to fix PortAudio stream stop()/close() on a hotkey-callback thread can freeze the global hotkey listener
  • portaudio close hotkey callback thread freeze global listener
  • portaudio close hotkey callback thread freeze global listener fix
  • freeze global listener stream stop close hotkey callback thread portaudio
  • freeze global listener stream stop close hotkey callback thread portaudio fix

Record metadata

Statusverified
Confidencemedium
Created2026-06-21
Updated2026-06-21
Last verified2026-06-21
Affected toolsportaudio, pynput, voice-to-claude
Tagsmacos, audio, portaudio, pynput, threading, hotkey, deadlock, voice

Common search queries

  • portaudio-stream-close-blocks-hotkey-callback-thread
  • portaudio stream close blocks hotkey callback thread
  • PortAudio stream stop()/close() on a hotkey-callback thread can freeze the global hotkey listener
  • PortAudio stream stop()/close() on a hotkey-callback thread can freeze the global hotkey listener fix
  • PortAudio stream stop()/close() on a hotkey-callback thread can freeze the global hotkey listener root cause
  • macos
  • audio
  • portaudio
  • pynput
  • threading
  • hotkey
  • deadlock

Symptoms

  • a push-to-talk dictation daemon intermittently goes unresponsive after switching windows and dictating
  • the log is stuck at 'Recording stopped, processing...' with no following 'Audio duration' line
  • daemon status still reports Running but pressing the hotkey does nothing
  • only a daemon restart recovers it

Environment

osmacOS
archApple Silicon
languagePython
runtimesounddevice + PortAudio, pynput global hotkey Listener
versions{"python":"3.12"}
constraintsdisplay-connected (Studio Display) mic, single pynput listener thread

Root cause

  • stop()/close() of a PortAudio stream on a flaky/display-connected device can block for a long time.
  • It was being called inline inside the pynput key-release callback, which runs on the single listener thread.
  • The blocking call wedged that thread, so the global hotkey stopped firing and the whole daemon appeared hung even though the process stayed alive.

Fix

  1. Move recorder.stop() (the PortAudio stream.stop()/close() calls) AND all downstream processing off the pynput key-release callback thread into a background thread.

  2. Make the callback only spawn the worker and return immediately, so the listener thread is never blocked.

Verification

  • Use the daemon heavily across multiple windows after the change.

    Expected: Audio-device stalls no longer freeze the hotkey.

Workarounds

  • Add a watchdog that restarts the listener if the hotkey stops responding.

Anti-patterns

  • Doing blocking audio teardown or processing inline in a global-hotkey callback.
  • Trusting 'process is Running' as proof the hotkey listener is still alive.

Sources

  • voice-to-claude daemon debugging session (local-session): local-session:2026-06-21

No matching fix?

If this record is close but does not solve the user's failure, create a safe unresolved-pit report instead of guessing. Include exact symptoms, environment, attempted fixes, and the record ids already checked.