Skip to content

AudioWorkletNode, AudioContext, AnalyserNode and MediaStreamAudioSourceNode leak on every mute/unmute cycle #1969

Description

@OTaran2107

Describe the bug

Each mute/unmute cycle of a local microphone track creates new AudioWorkletNode, AudioContext, AnalyserNode, and MediaStreamAudioSourceNode instances that are not released after room.disconnect() and a forced garbage collection.

The leaked nodes persist even after:

  • room.disconnect() is called
  • 60+ seconds have passed
  • window.gc() (Chrome DevTools forced GC) is triggered
  • The heap snapshot is taken

Reproduction

  1. Open https://meet.livekit.io/ in Chrome with DevTools open
  2. Take a baseline heap snapshot (before joining) → note AudioWorkletNode = 1
  3. Join a room as a single participant
  4. Mute and unmute the microphone several times
  5. Take a during-call heap snapshot
  6. Leave the room
  7. Wait 60 seconds
  8. Force GC in DevTools → Memory → Collect garbage
  9. Take an after heap snapshot
  10. Compare before → after

Expected Behavior

All AudioWorkletNode, AudioContext, AnalyserNode, and MediaStreamAudioSourceNode instances created during the call should be released after room.disconnect() and GC. The after-call count should match the before-call baseline.

System Info

livekit-client: latest (tested at meet.livekit.io), in our app used 2.19.1 - the same problem detected
Browser: Chrome 124+
Platform: Web
Reproducible on: meet.livekit.io (official demo, no custom code)

Severity

serious, but I can work around it

Additional Information

Nodes accumulate with each mute/unmute cycle and are not collected even after forced GC:

Object Before During After GC Delta
AudioWorkletNode 1 6 6 +5
AudioContext 1 4 4 +3
AnalyserNode 1 4 4 +3
MediaStreamAudioSourceNode 1 5 5 +4
MediaStreamAudioDestinationNode 1 5 5 +4
RTCRtpSender 1 16 9 +8
RTCRtpReceiver 1 16 9 +8
EventListener 345 531 463 +118

JS heap before: 8.1 MB → after GC: 37.3 MB (+29.2 MB, +358%)

Note: RTCRtpSender/Receiver values shown above are from the LiveKit demo. In our own application with proper teardown order (removeTrack() before close()), these objects are collected by GC — so that part may be a teardown ordering issue rather than an SDK-level leak. The AudioWorkletNode group however persists regardless.

Heap Snapshot Evidence

Snapshots taken at https://meet.livekit.io/no third-party code, no custom audio processors, no noise cancellation. Pure LiveKit SDK only.

Filter AudioWorkletNode in Chrome DevTools → Memory → Heap snapshot → Class filter:

  • Before call: 1 instance
  • After call + forced GC: 6 instances (+5)

The retaining path shows the leaked nodes are held by the Web Audio graph that was created during the mute/unmute cycle but not torn down.

Suspected Root Cause

When the local microphone track is muted, the SDK appears to create a new AudioContext + AudioWorkletNode graph for the replacement silent track, but does not call audioContext.close() or node.disconnect() on the previous graph before replacing it. Over multiple mute/unmute cycles, old graphs accumulate.

Suspected location: audio processing pipeline in LocalAudioTrack or LocalTrackProcessor — specifically the path that rebuilds the audio graph on mute() / setProcessor().

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions