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().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions