Describe the bug
Setting disconnectOnPageLeave: false when creating a Room does not prevent the SDK from calling room.disconnect() when the browser freezes a background tab via the Page Lifecycle API freeze event. The room is torn down silently and the participant is kicked without any disconnect reason being surfaced to the app.
Root Cause
The SDK unconditionally registers room.onPageLeave as a listener on both pagehide/beforeunload AND window 'freeze'. The disconnectOnPageLeave flag correctly gates the pagehide/beforeunload path, but the freeze listener is always attached regardless of this flag.
Inspecting the SDK source (simplified):
// SDK internal setup — always runs regardless of disconnectOnPageLeave
window.addEventListener('freeze', this.onPageLeave);
// Only gated by disconnectOnPageLeave:
if (this.options.disconnectOnPageLeave) {
window.addEventListener('pagehide', this.onPageLeave);
window.addEventListener('beforeunload', this.onPageLeave);
}
So disconnectOnPageLeave: false has no effect on the freeze path.
Why this matters in production
Edge's Sleeping Tabs / Efficiency Mode aggressively freezes background tabs that have no active media session. The key asymmetry in a classroom app:
Teacher tab: publishing audio/video → browser recognises an active media session → tab is never frozen
Student tab (viewer-only, mic/camera off): no active media session → tab is frozen when the student switches to another window
The result: students who switch away from the classroom tab are silently kicked. Teachers are completely immune. This is extremely hard to debug because it looks like a random WebSocket drop with no reason code.
Minimal reproduction
import { Room, RoomEvent } from 'livekit-client';
const room = new Room();
await room.connect(WS_URL, TOKEN, {
disconnectOnPageLeave: false, // <-- should prevent SDK-initiated disconnects on nav/freeze
});
room.on(RoomEvent.Disconnected, (reason) => {
console.log('disconnected:', reason);
// Fires with reason=undefined when Edge freezes the tab
// Expected: should NOT fire because disconnectOnPageLeave is false
});
// Reproduce:
// 1. Join a room from Edge (Sleeping Tabs on by default)
// 2. Switch to a different tab for ~2+ minutes
// 3. Return — room is gone, Disconnected event already fired
Workaround we implemented
We work around it by manually removing the freeze listener after every successful connect. We access room.onPageLeave via a cast because it is not exposed on the public type:
function NoFreezeGuard(): null {
const room = useRoomContext();
useEffect(() => {
const removeFreezeListener = (): void => {
// room.onPageLeave is the bound arrow function the SDK registered.
const handler = (room as any).onPageLeave as EventListener | undefined;
if (handler) window.removeEventListener('freeze', handler);
};
// Run after every RoomEvent.Connected (initial connect + Phase 2 remounts).
room.on(RoomEvent.Connected, removeFreezeListener);
// Also run immediately in case Connected already fired before this effect.
removeFreezeListener();
return () => { room.off(RoomEvent.Connected, removeFreezeListener); };
}, [room]);
return null;
}
This works, but it relies on an internal private property name (onPageLeave) that could break across SDK releases. It should not be necessary.
Expected behaviour
When disconnectOnPageLeave: false is set, the SDK should not call room.disconnect() for any page lifecycle event — including freeze. The intent of the option is clearly "do not disconnect when the user navigates or backgrounds the tab"; the freeze event is squarely within that intent.
Questions for the team
Was the freeze listener intentionally excluded from the disconnectOnPageLeave gate, and if so, why?
Is there a supported API to opt out of the freeze listener without reaching into private properties?
On unfreeze, does the SDK's Phase 1 reconcile/reconnect mechanism attempt to restore the WebSocket if the connection was lost while frozen? In our testing it does not — the connection is already gone because disconnect() was called deliberately before the freeze completed.
Would you consider either:
Gating the freeze listener on disconnectOnPageLeave alongside pagehide/beforeunload
Exposing a separate disconnectOnFreeze option (useful for apps that want different behaviour on tab-freeze vs. full page navigation)
Additional context
A secondary issue compounds this: if Phase 2 (full room remount with a new token) kicks in while the tab is still frozen, setTimeout-based retry delays are throttled to a minimum 1-minute interval by the browser's background timer throttling. We worked around this by resolving delays immediately on visibilitychange → visible, but the root issue (the freeze disconnect) is what we'd like fixed upstream.
Reproduction
Can we connect over meet so that i can show you the issue that is happening.
Logs
This disconnect does not produce server-side logs — the SDK calls room.disconnect()
synchronously in the browser's freeze event handler before any network request
is made. The only evidence is in the browser's DevTools console.
With setLogLevel('debug') enabled, the sequence immediately before disconnect is:
[LK] ℹ room state changed disconnected
[LK] ℹ RoomEvent.Disconnected reason=undefined
There is no preceding WebSocket error, no close frame, and no onDisconnected
reason code — the connection is terminated client-side by the SDK's own
freeze listener before the server is aware anything happened.
Our production app confirmed the behaviour: students returned to the classroom
tab to find themselves in a disconnected state with no error shown, and the
server-side logs showed zero disconnect events from LiveKit webhooks around
that time — consistent with a client-initiated disconnect that the server
treats as a clean leave.
System Info
OIT LMS — System Info
System
OS Ubuntu 24.04.2 LTS (Noble Numbat) — Linux 6.17
CPU 4× Intel Xeon Platinum 8581C @ 2.30GHz (x64)
Memory 3.19 GB / 7.75 GB
Environment Replit container
Shell Bash 5.2.21
Binaries
Node.js 24.13.0
pnpm 10.26.1
npm 11.6.2
Yarn 1.22.22
Bun 1.3.6
LiveKit Packages
Package Version
livekit-client ^2.18.7
livekit-server-sdk ^2.15.2
@livekit/components-react ^2.9.20
@livekit/components-styles ^1.2.0
@livekit/krisp-noise-filter ^0.4.3
App Stack
Frontend React 18 + Vite + Wouter + TanStack Query v5
Backend Express 5
Database PostgreSQL + Drizzle ORM
TypeScript 5.9
Package manager pnpm workspaces 10.26.1
Severity
blocking an upgrade
Additional Information
No response
Describe the bug
Setting disconnectOnPageLeave: false when creating a Room does not prevent the SDK from calling room.disconnect() when the browser freezes a background tab via the Page Lifecycle API freeze event. The room is torn down silently and the participant is kicked without any disconnect reason being surfaced to the app.
Root Cause
The SDK unconditionally registers room.onPageLeave as a listener on both pagehide/beforeunload AND window 'freeze'. The disconnectOnPageLeave flag correctly gates the pagehide/beforeunload path, but the freeze listener is always attached regardless of this flag.
Inspecting the SDK source (simplified):
// SDK internal setup — always runs regardless of disconnectOnPageLeave
window.addEventListener('freeze', this.onPageLeave);
// Only gated by disconnectOnPageLeave:
if (this.options.disconnectOnPageLeave) {
window.addEventListener('pagehide', this.onPageLeave);
window.addEventListener('beforeunload', this.onPageLeave);
}
So disconnectOnPageLeave: false has no effect on the freeze path.
Why this matters in production
Edge's Sleeping Tabs / Efficiency Mode aggressively freezes background tabs that have no active media session. The key asymmetry in a classroom app:
Teacher tab: publishing audio/video → browser recognises an active media session → tab is never frozen
Student tab (viewer-only, mic/camera off): no active media session → tab is frozen when the student switches to another window
The result: students who switch away from the classroom tab are silently kicked. Teachers are completely immune. This is extremely hard to debug because it looks like a random WebSocket drop with no reason code.
Minimal reproduction
import { Room, RoomEvent } from 'livekit-client';
const room = new Room();
await room.connect(WS_URL, TOKEN, {
disconnectOnPageLeave: false, // <-- should prevent SDK-initiated disconnects on nav/freeze
});
room.on(RoomEvent.Disconnected, (reason) => {
console.log('disconnected:', reason);
// Fires with reason=undefined when Edge freezes the tab
// Expected: should NOT fire because disconnectOnPageLeave is false
});
// Reproduce:
// 1. Join a room from Edge (Sleeping Tabs on by default)
// 2. Switch to a different tab for ~2+ minutes
// 3. Return — room is gone, Disconnected event already fired
Workaround we implemented
We work around it by manually removing the freeze listener after every successful connect. We access room.onPageLeave via a cast because it is not exposed on the public type:
function NoFreezeGuard(): null {
const room = useRoomContext();
useEffect(() => {
const removeFreezeListener = (): void => {
// room.onPageLeave is the bound arrow function the SDK registered.
const handler = (room as any).onPageLeave as EventListener | undefined;
if (handler) window.removeEventListener('freeze', handler);
};
// Run after every RoomEvent.Connected (initial connect + Phase 2 remounts).
room.on(RoomEvent.Connected, removeFreezeListener);
// Also run immediately in case Connected already fired before this effect.
removeFreezeListener();
return () => { room.off(RoomEvent.Connected, removeFreezeListener); };
}, [room]);
return null;
}
This works, but it relies on an internal private property name (onPageLeave) that could break across SDK releases. It should not be necessary.
Expected behaviour
When disconnectOnPageLeave: false is set, the SDK should not call room.disconnect() for any page lifecycle event — including freeze. The intent of the option is clearly "do not disconnect when the user navigates or backgrounds the tab"; the freeze event is squarely within that intent.
Questions for the team
Was the freeze listener intentionally excluded from the disconnectOnPageLeave gate, and if so, why?
Is there a supported API to opt out of the freeze listener without reaching into private properties?
On unfreeze, does the SDK's Phase 1 reconcile/reconnect mechanism attempt to restore the WebSocket if the connection was lost while frozen? In our testing it does not — the connection is already gone because disconnect() was called deliberately before the freeze completed.
Would you consider either:
Gating the freeze listener on disconnectOnPageLeave alongside pagehide/beforeunload
Exposing a separate disconnectOnFreeze option (useful for apps that want different behaviour on tab-freeze vs. full page navigation)
Additional context
A secondary issue compounds this: if Phase 2 (full room remount with a new token) kicks in while the tab is still frozen, setTimeout-based retry delays are throttled to a minimum 1-minute interval by the browser's background timer throttling. We worked around this by resolving delays immediately on visibilitychange → visible, but the root issue (the freeze disconnect) is what we'd like fixed upstream.
Reproduction
Can we connect over meet so that i can show you the issue that is happening.
Logs
System Info
Severity
blocking an upgrade
Additional Information
No response