I keep seeing people wire up their own ICE restart logic on top of LiveKit after a call drops on a network switch. You don’t need to the SDK already does it, and hand-rolling it on top usually makes things worse. Here’s what actually happens and where the real gaps are.
What LiveKit does for you
On a network change it reconnects the signalling WebSocket and initiates an ICE restart itself. Most of the time the user sees nothing, or a second or two of frozen video. Only if that resume path fails does it fall back to a full reconnect. (docs)
So if you’re listening for iceconnectionstatechange and calling your own restart, you’re racing the SDK’s recovery, not helping it.
Bite #1: you’re probably listening for the wrong event
RoomEvent.Reconnecting is deliberately not emitted for resumes that changed in #1012. So if your “reconnecting…” spinner is driven off Reconnecting, it stays invisible during the exact scenario you built it for, because the resume path handles most WiFi→cellular switches.
The event you actually want on livekit-client is RoomEvent.SignalReconnecting, which fires when the signal connection drops and resolves with Reconnected. Per its docstring, if media fails on top of that, you then get Reconnecting as well so the two are a ladder, not alternatives.
Connection quality is the other early signal: a value of Lost surfaces before either Reconnecting or Disconnected and means the peer connection to the SFU has actually dropped.
Bite #2: a full reconnect is a rejoin, and it replays every join event
The docs are explicit that the sequence is identical to everyone leaving the room and coming back:
ParticipantDisconnectedfor everyone elseLocalTrackUnpublishedfor any unpublished tracksReconnecting- full reconnect
ReconnectedParticipantConnectedfor everyone currently in the room- local tracks republished →
LocalTrackPublished
Steps 6 and 7 are what break apps. Any state you build in a ParticipantConnected or LocalTrackPublished handler track refs, subscription bookkeeping, analytics counters, agent session state gets built a second time. If those handlers aren’t idempotent you end up with duplicate tiles, double-counted sessions, or two renderers fighting over one track. It looks like a rendering bug and it’s really a reconnect.
Keying that state by participant identity and track SID rather than appending on each event fixes it, and it costs nothing when no reconnect happens.
On TURN
Worth knowing the fallback order, because “it works on my WiFi and dies on corporate” is almost always this: ICE/UDP → TURN/UDP 3478 → ICE/TCP → TURN/TLS. On Cloud all four are provided. Self-hosted, only the first two work out of the box TURN needs its own config and certificate, and TURN/TLS on 443 is what saves you behind firewalls that only allow outbound TLS.