Realtime moderator disconnects in a gaming voice lobby need observability signals that separate media health from moderation authority. A moderator's browser is inside the trust boundary for the former, but outside it for the latter.
Short answer: detect a realtime moderator disconnect by correlating an authenticated lobby lease with WebRTC connection-state transitions and periodic getStats() samples, then alert only when those independent signals agree long enough to rule out a brief network transition.
Don't reduce the problem to a single “offline” event. A tab can stop reporting, a peer connection can move through disconnected, and a user can lose the moderator role while media still flows. Those are different conditions with different responses. The useful output is a small state machine that says what the service knows, how it knows it, and when the evidence became stale.
How should a realtime gaming voice lobby observe moderator disconnects?
Start with three separate questions: does the moderator still hold an authorized lobby lease, is the moderator's peer connection usable, and is the client still producing fresh telemetry? The first belongs to the trusted application service. The second comes from the browser's RTCPeerConnection. The third is a statement about observability freshness, not user presence.
The W3C WebRTC Recommendation defines connectionState as an aggregate state over the peer connection's ICE transports and DTLS transports. Its values include new, connecting, connected, disconnected, failed, and closed. It also defines the connectionstatechange event and getStats(). Those are the standards-based raw materials; the final “moderator absent” decision remains application policy.
That distinction matters. Treating disconnected as an immediate eviction signal makes a transient transport change indistinguishable from a moderator leaving. Treating a missing client heartbeat as proof of departure gives an untrusted, possibly suspended tab authority over lobby control. Use client events as evidence, never as the source of moderation authority.
The token should therefore be narrow: one lobby, one participant, one role, and a bounded lifetime. The backend should derive the active role from the validated token and its own lease record rather than accepting role: "moderator" from an event body. A reconnect may resume observation, but it should not silently widen scope or revive an expired role.
One subtle trap is naming every signal “presence.” That creates an attractive dashboard and an ambiguous incident. Name the actual state instead: lease_active, telemetry_fresh, and peer_connection_state. The alert can then explain itself: “authorized moderator lease is active, telemetry is stale, last peer state was connected.”
Keep the states separate.
Build the evidence record before the alert
A focused event record needs a server-resolved lobby and subject, a server receive time, the client observation time, the WebRTC connection state, and a monotonically increasing sequence number. The receive time is the freshness clock because a client clock can drift. The observation time is still useful for debugging ordering. Sequence numbers expose duplicates and late delivery without pretending that timestamps are perfectly synchronized.
For privacy and cardinality, don't attach raw audio, display names, full tokens, SDP, ICE candidates, or arbitrary exception strings to routine telemetry. A lobby identifier and pseudonymous participant identifier are enough for correlation in most systems. Keep high-cardinality values out of metric labels; retain them in bounded event logs or traces where access controls and retention rules can be applied.
Here is a runnable Python model of the server-side decision. The ten-second freshness window is an example configuration value, not a WebRTC standard. Tune it against the reconnect behavior and false-alert budget of the actual lobby.
from dataclasses import dataclass
from enum import Enum
from time import monotonic
class PeerState(str, Enum):
NEW = "new"
CONNECTING = "connecting"
CONNECTED = "connected"
DISCONNECTED = "disconnected"
FAILED = "failed"
CLOSED = "closed"
@dataclass(frozen=True)
class ModeratorEvidence:
lobby_id: str
subject_id: str
token_lobby_id: str
token_role: str
lease_active: bool
peer_state: PeerState
received_at: float
sequence: int
def classify(evidence: ModeratorEvidence, now: float, stale_after: float = 10.0) -> str:
token_is_scoped = (
evidence.token_lobby_id == evidence.lobby_id
and evidence.token_role == "moderator"
)
if not token_is_scoped or not evidence.lease_active:
return "no_authorized_moderator"
telemetry_is_fresh = now - evidence.received_at <= stale_after
if not telemetry_is_fresh:
return "moderator_telemetry_stale"
if evidence.peer_state in {PeerState.FAILED, PeerState.CLOSED}:
return "moderator_connection_unavailable"
if evidence.peer_state == PeerState.DISCONNECTED:
return "moderator_connection_degraded"
return "moderator_observed"
if __name__ == "__main__":
current = monotonic()
sample = ModeratorEvidence(
lobby_id="lobby-42",
subject_id="user-7",
token_lobby_id="lobby-42",
token_role="moderator",
lease_active=True,
peer_state=PeerState.DISCONNECTED,
received_at=current - 2.0,
sequence=184,
)
print(classify(sample, current))
The sample prints moderator_connection_degraded. Walk through the evidence rather than jumping to “offline”: sequence 184 arrived two seconds ago, its token is scoped to lobby-42, the trusted lease is active, and the peer state is disconnected. The detector therefore records degradation but does not revoke the role, claim the user left, or page an operator. If sequence 185 reports connected before the example ten-second freshness window closes, the next evaluation returns moderator_observed and the transition can be counted as a recovery. If no newer sequence arrives, freshness eventually becomes the stronger signal and the classification changes to moderator_telemetry_stale. A closed report instead produces moderator_connection_unavailable. This restraint is intentional: each output describes exactly what the collected evidence supports, while alert routing remains a separately testable policy.
The simple approach fails because it compresses observation, authorization, and response into one boolean. The chosen approach keeps them independent until the decision point. It is a little more code — and much easier to evaluate.
Test transitions, silence, and hostile input
Notebook experiments tend to exercise clean rows: connected, disconnected, closed. Production evaluation needs sequences. Replay ordered events such as connected → disconnected → connected, then inject a duplicate sequence number, a delayed sample, and complete silence. The expected result should specify both the classification and whether an alert opens, remains pending, or resolves.
Add adversarial cases around token scope. Send an otherwise valid event whose token names another lobby. Send a participant token with a body claiming the moderator role. Expire the lease while a fresh connected report remains in flight. Each case should fail closed at the authority layer while preserving enough structured evidence to diagnose why. This is where client trust becomes testable rather than rhetorical.
An eval table can stay compact:
| Input sequence | Trusted lease | Expected classification |
|---|---|---|
connected |
active | moderator_observed |
disconnected → connected inside the grace window |
active | degraded, then observed |
| no fresh telemetry beyond the window | active | moderator_telemetry_stale |
fresh connected from the wrong lobby scope |
active elsewhere | no_authorized_moderator |
closed |
active | moderator_connection_unavailable |
Do not freeze the grace window because one test fixture used ten seconds. Measure the distribution of degraded-state duration, the fraction that recover without intervention, and the time between the final fresh sample and a confirmed lease change. I'm not sure a single threshold can serve mobile backgrounding, desktop network handoff, and an intentionally closed tab equally well; segmented production evidence would resolve that uncertainty.
Short tests catch logic errors. Long replays catch policy errors.
For alert evaluation, track precision against a trusted outcome such as lease expiration or an explicit server-processed leave. Also track detection delay. Optimizing only for speed produces noisy pages; optimizing only for precision can leave a lobby unmanaged for too long. The acceptable balance is a product and safety decision, so encode it in the eval harness instead of burying it in an arbitrary timer.
Operate the detector without turning telemetry into a bill spike
getStats() returns a promise containing an RTCStatsReport, and an application may select the peer connection or a specific track when requesting statistics. That does not imply every report belongs in storage. Sample at a deliberate interval, extract only the fields required by an eval or operational question, and increase detail temporarily when the state changes. Event-driven connection-state records plus sparse periodic samples usually express the incident more clearly than dumping every available statistic.
Prompt-cost awareness applies even if an AI system only summarizes incidents after the fact. Never stream raw stats reports into a model. Build a deterministic incident envelope first: scoped lobby, pseudonymous subject, lease status, state transition sequence, freshness, and the detector's reason code. A model can summarize that bounded record, while the authorization and alert decision remain ordinary code that can be replayed exactly.
The catch is that this design is not suitable when the application service cannot own a trusted lease or role record. In that environment, the detector can report client connectivity but cannot prove that a lobby lacks an authorized moderator. Fix the authority model first. Likewise, stick with a simpler connection-health dashboard when moderator absence has no automated safety or control consequence; a multi-signal pager would add operational cost without improving a decision.
Before copying this architecture, measure four things: transient disconnected duration, telemetry delivery lag, false alerts per lobby-hour, and time to detect a genuinely absent moderator. Also inspect metric cardinality and retained event volume. Those numbers determine the sampling interval and grace window; the WebRTC specification supplies observable states, not universal alert thresholds.













