Long-idle React Native connections degrade: subscribed tracks stop binding, followed by cascading renegotiation failures and internal-resume races (sometimes ending in DUPLICATE_IDENTITY)
Summary
In a PTT (push-to-talk) style application — long-lived room connections, mostly idle, sporadic short audio bursts, app frequently backgrounded — we consistently observe, only on React Native (Android) clients using @livekit/react-native-webrtc, that after the connection has been alive for some minutes (observed anywhere from ~3 to ~45 minutes across different tests, so not a fixed timeout), one of the following starts happening:
- A subscribed remote track silently stops working: server logs
"track not bound after timeout" (rtc/subscriptionmanager.go:576) repeatedly for that trackID/participant, and the client's getStats()/final track stats for that track show packetsDiscarded ≈ packetsReceived and concealedSamples a large fraction of totalSamplesReceived — i.e. bytes/packets are arriving but essentially none of it is being decoded into real audio.
- Shortly after (seconds to a couple minutes), the same PeerConnection starts emitting repeated
"negotiation disconnected" events, sometimes followed by NegotiationError: negotiation timed out (code 13).
- This triggers the engine's own internal
reconnectPolicy/resume. The resume itself frequently fails ("Got disconnected during reconnection attempt", "detected connection state mismatch", "already attempting reconnect, returning early"), and the client falls back to a full fresh reconnect with a brand new participantID.
- On the server, we have directly observed this fresh-reconnect racing with the not-yet-fully-torn-down previous session, producing
DUPLICATE_IDENTITY (service/roommanager.go:385 "removing duplicate participant", then rtc/participant.go:1417 "participant closing" reason: "DUPLICATE_IDENTITY"), even though only ONE physical client/device was ever involved (no duplicate login, no second device).
Crucially: the same room, same server, same time window, with a web browser client (regular livekit-client in Chrome) connected simultaneously as another participant never exhibits this — it stays connected and its tracks never fail to bind. Only the React Native / Android clients degrade this way. This strongly suggests the issue lives in the RN <-> native WebRTC binding layer (or in how the JS engine drives it under Android background/Doze conditions), not in the SFU/server or in generic livekit-client logic.
Environment
livekit-client: 2.22.1
@livekit/react-native-webrtc: 144.1.2
@livekit/react-native: 2.12.0
- LiveKit server: self-hosted, v1.13.1
- Client OS: Android (multiple devices reproduced: a Samsung phone, an Android emulator, and industrial rugged radios — all React Native, all Android)
- Room config:
adaptiveStream: false, dynacast: false, custom rtcConfig (iceConnectionTimeout: 15000, iceTransportPolicy: 'relay'), reconnectPolicy.maxRetries tested at both 1 and Infinity — the degradation itself is not prevented by either value, only its downstream shape changes.
What we've already ruled out / confirmed via source inspection
- Not a network issue: reproduced on strong Wi-Fi (
rssi around -18 to -25 dBm) and on pure cellular data, from multiple physical locations/networks (Portugal, Brazil).
- Not caused by our own application code racing itself: we ran a control test with every custom reconnect trigger in our app disabled, and the SDK's own internal resume alone still produced
DUPLICATE_IDENTITY.
- Found via reading the bundled
livekit-client UMD source that RoomOptions/connectOptions.peerConnectionTimeout (passed to the initial room.connect()) is copied only onto RTCEngine.peerConnectionTimeout, but the actual negotiation-timeout timer that fires "negotiation timed out" lives on a separate object, PCTransportManager (this.pcManager), which is constructed with no timeout parameter and always defaults to the hardcoded 15000ms (Nd.peerConnectionTimeout in the minified bundle) — meaning a custom, longer peerConnectionTimeout is silently not honored by internal reconnects/resumes, only by the very first room.connect() call. We worked around this by manually patching room.engine.pcManager.peerConnectionTimeout after connect and on every Reconnecting/Reconnected event, which helped but did not eliminate the underlying degradation — it just changed how long it takes to surface.
Reproduction
Not yet a minimal repro app — this was found through real production traffic capture (client adb logcat + server LiveKit container logs, timestamps cross-referenced). We are actively narrowing this down and can provide:
- Full client-side
adb logcat excerpts (ReactNativeJS + rn-webrtc:pc debug lines) covering the transition into failure.
- Matching server-side LiveKit container logs for the same time window (UTC), showing
"track not bound after timeout", "removing duplicate participant", and "participant closing" reason: DUPLICATE_IDENTITY".
We're happy to build a minimal reproduction (idle room + one RN Android publisher/subscriber, left running for 10-45 minutes) if that's useful — let us know the preferred format for that.
Questions
- Is this a known issue with
@livekit/react-native-webrtc on long-idle connections? Any existing tracking issue we should link to instead?
- Is there a supported way to keep a long-idle RN PeerConnection healthy — e.g. a documented keep-alive/renegotiation pattern for PTT-style (idle-then-burst, half-duplex) traffic, as opposed to continuous conferencing traffic?
- Is
pcManager.peerConnectionTimeout intentionally decoupled from connectOptions.peerConnectionTimeout, or is that a bug we should file separately?
Happy to provide any additional logs/detail needed.
Long-idle React Native connections degrade: subscribed tracks stop binding, followed by cascading renegotiation failures and internal-resume races (sometimes ending in DUPLICATE_IDENTITY)
Summary
In a PTT (push-to-talk) style application — long-lived room connections, mostly idle, sporadic short audio bursts, app frequently backgrounded — we consistently observe, only on React Native (Android) clients using
@livekit/react-native-webrtc, that after the connection has been alive for some minutes (observed anywhere from ~3 to ~45 minutes across different tests, so not a fixed timeout), one of the following starts happening:"track not bound after timeout"(rtc/subscriptionmanager.go:576) repeatedly for that trackID/participant, and the client'sgetStats()/final track statsfor that track showpacketsDiscarded≈packetsReceivedandconcealedSamplesa large fraction oftotalSamplesReceived— i.e. bytes/packets are arriving but essentially none of it is being decoded into real audio."negotiation disconnected"events, sometimes followed byNegotiationError: negotiation timed out(code 13).reconnectPolicy/resume. The resume itself frequently fails ("Got disconnected during reconnection attempt","detected connection state mismatch","already attempting reconnect, returning early"), and the client falls back to a full fresh reconnect with a brand newparticipantID.DUPLICATE_IDENTITY(service/roommanager.go:385 "removing duplicate participant", thenrtc/participant.go:1417 "participant closing" reason: "DUPLICATE_IDENTITY"), even though only ONE physical client/device was ever involved (no duplicate login, no second device).Crucially: the same room, same server, same time window, with a web browser client (regular
livekit-clientin Chrome) connected simultaneously as another participant never exhibits this — it stays connected and its tracks never fail to bind. Only the React Native / Android clients degrade this way. This strongly suggests the issue lives in the RN <-> native WebRTC binding layer (or in how the JS engine drives it under Android background/Doze conditions), not in the SFU/server or in genericlivekit-clientlogic.Environment
livekit-client: 2.22.1@livekit/react-native-webrtc: 144.1.2@livekit/react-native: 2.12.0adaptiveStream: false,dynacast: false, customrtcConfig(iceConnectionTimeout: 15000,iceTransportPolicy: 'relay'),reconnectPolicy.maxRetriestested at both1andInfinity— the degradation itself is not prevented by either value, only its downstream shape changes.What we've already ruled out / confirmed via source inspection
rssiaround -18 to -25 dBm) and on pure cellular data, from multiple physical locations/networks (Portugal, Brazil).DUPLICATE_IDENTITY.livekit-clientUMD source thatRoomOptions/connectOptions.peerConnectionTimeout(passed to the initialroom.connect()) is copied only ontoRTCEngine.peerConnectionTimeout, but the actual negotiation-timeout timer that fires"negotiation timed out"lives on a separate object,PCTransportManager(this.pcManager), which is constructed with no timeout parameter and always defaults to the hardcoded15000ms(Nd.peerConnectionTimeoutin the minified bundle) — meaning a custom, longerpeerConnectionTimeoutis silently not honored by internal reconnects/resumes, only by the very firstroom.connect()call. We worked around this by manually patchingroom.engine.pcManager.peerConnectionTimeoutafter connect and on everyReconnecting/Reconnectedevent, which helped but did not eliminate the underlying degradation — it just changed how long it takes to surface.Reproduction
Not yet a minimal repro app — this was found through real production traffic capture (client
adb logcat+ server LiveKit container logs, timestamps cross-referenced). We are actively narrowing this down and can provide:adb logcatexcerpts (ReactNativeJS + rn-webrtc:pc debug lines) covering the transition into failure."track not bound after timeout","removing duplicate participant", and"participant closing" reason: DUPLICATE_IDENTITY".We're happy to build a minimal reproduction (idle room + one RN Android publisher/subscriber, left running for 10-45 minutes) if that's useful — let us know the preferred format for that.
Questions
@livekit/react-native-webrtcon long-idle connections? Any existing tracking issue we should link to instead?pcManager.peerConnectionTimeoutintentionally decoupled fromconnectOptions.peerConnectionTimeout, or is that a bug we should file separately?Happy to provide any additional logs/detail needed.