Skip to content

Long-idle connections degrade: subscribed tracks stop binding, followed by cascading renegotiation failures and internal-resume races #451

Description

@dabmax

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:

  1. 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 packetsDiscardedpacketsReceived and concealedSamples a large fraction of totalSamplesReceived — i.e. bytes/packets are arriving but essentially none of it is being decoded into real audio.
  2. 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).
  3. 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.
  4. 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

  1. Is this a known issue with @livekit/react-native-webrtc on long-idle connections? Any existing tracking issue we should link to instead?
  2. 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?
  3. 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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    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