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
- Open https://meet.livekit.io/ in Chrome with DevTools open
- Take a baseline heap snapshot (before joining) → note
AudioWorkletNode = 1
- Join a room as a single participant
- Mute and unmute the microphone several times
- Take a during-call heap snapshot
- Leave the room
- Wait 60 seconds
- Force GC in DevTools → Memory → Collect garbage
- Take an after heap snapshot
- 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().
Describe the bug
Each mute/unmute cycle of a local microphone track creates new
AudioWorkletNode,AudioContext,AnalyserNode, andMediaStreamAudioSourceNodeinstances that are not released afterroom.disconnect()and a forced garbage collection.The leaked nodes persist even after:
room.disconnect()is calledwindow.gc()(Chrome DevTools forced GC) is triggeredReproduction
AudioWorkletNode = 1Expected Behavior
All
AudioWorkletNode,AudioContext,AnalyserNode, andMediaStreamAudioSourceNodeinstances created during the call should be released afterroom.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:
AudioWorkletNodeAudioContextAnalyserNodeMediaStreamAudioSourceNodeMediaStreamAudioDestinationNodeRTCRtpSenderRTCRtpReceiverEventListenerJS heap before: 8.1 MB → after GC: 37.3 MB (+29.2 MB, +358%)
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
AudioWorkletNodein Chrome DevTools → Memory → Heap snapshot → Class filter: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+AudioWorkletNodegraph for the replacement silent track, but does not callaudioContext.close()ornode.disconnect()on the previous graph before replacing it. Over multiple mute/unmute cycles, old graphs accumulate.Suspected location: audio processing pipeline in
LocalAudioTrackorLocalTrackProcessor— specifically the path that rebuilds the audio graph onmute()/setProcessor().