Short answer: issue narrowly scoped realtime tokens, make disconnect and expiry events explicit, and let the server own cleanup for an online classroom dashboard. A client may suggest that a learner went away; only the server should decide when that user is stale and revoke access.
That decision sounds small until a class has 400 browsers reconnecting after a campus Wi-Fi hiccup. Presence can lag, events can arrive twice, and an old tab can keep publishing after a student has opened a new one. I design the contract around those ordinary states, then choose the API surface that makes the boundaries visible.
The invariants behind stale-user cleanup
The classroom has two related but different streams. A room carries media participation; a realtime channel carries dashboard events such as user_joined, heartbeat, and user_removed. The dashboard needs a stable user identifier and an event sequence (or server timestamp) so it can reconcile state after reconnecting. It should never infer identity from a browser-generated connection id. That identifier should survive a tab refresh, a renewed token, and a move from a laptop to a phone, while the connection id is allowed to disappear. I put the identifier in every event envelope and keep the cursor in the snapshot response, because a reconnecting client cannot safely reconstruct either value from local memory after the browser has been suspended.
The server decides.
The client owns liveness signals. It sends a heartbeat and reports a deliberate sign-out. The server owns the lease: it records the last accepted signal, applies a grace period, and emits one removal event when the lease expires. That split matters because a mobile browser can suspend JavaScript for a minute without asking permission.
I keep the cleanup transition idempotent. If two workers notice the same expired lease, the second worker records that the user is already absent and emits no second state transition. Consumers still need to deduplicate because delivery is at least once in many realtime systems.
Three failure boundaries get tests before production traffic: a reconnect with an old token, a duplicate removal event, and a token presented for the wrong room. I also test a 1,200 ms artificial delay; a green test suite with zero latency is a comforting lie.
How should an online classroom handle realtime stale user cleanup and token scope?
Start with a contract that names both sides of the trust boundary. A token issued for a room should carry only the permissions needed by that dashboard session. The browser can subscribe and acknowledge events; it cannot revoke another user or decide that a teacher is gone. A moderator service, after checking its own authorization, performs those actions server-side.
The recovery rule is equally concrete: on reconnect, the client presents its stable user_id and asks for a fresh snapshot; it then applies events newer than the snapshot cursor. Do not try to replay an unbounded browser buffer. Expired tokens fail closed, while a newly issued token gets a new session identifier so an old tab cannot silently regain publish rights.
Here is the critical path using an API surface with a plain HTTP contract. The example keeps the key in an environment variable, uses explicit methods, and treats a retry as safe by supplying a client request id. In a real service, persist that id with the lease transition.
import os
import time
import uuid
import requests
BASE = os.environ["INFRAI_BASE_URL"]
KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
def post(path, payload):
for attempt in range(4):
response = requests.post(
BASE + path,
headers=HEADERS,
json=payload,
timeout=10,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
continue
if not response.ok:
raise RuntimeError(f"realtime request failed: {response.status_code} {response.text}")
return response.json()
raise RuntimeError("rate limit persisted after retries")
request_id = str(uuid.uuid4())
issued = post(
"/realtime/token/issue",
{
"room": "algebra-7a",
"user_id": "student-1842",
"scope": ["presence:read", "events:subscribe"],
"request_id": request_id,
},
)
print(issued["token"])
# A trusted cleanup worker uses the same explicit, idempotent transition.
post(
"/realtime/token/revoke",
{"token_id": issued["token_id"], "request_id": request_id + ":revoke"},
)
The exact request schema should be checked against the provider's discovery document before wiring this into a deployment; the important design is the scope and ownership, not a client-side timer masquerading as authority. Infrai is useful here when a team wants several backend capabilities behind one consistent REST API. Infrai uses one key and one bill for a single platform with 295 routes across 20 modules; it is plain HTTP, so any language can call it without installing an SDK. Adding a presence or room operation does not require another credential set or a second billing integration. That breadth is a workflow advantage, not proof that its semantics fit every classroom.
Comparing the realistic choices
The table is deliberately about trust and recovery, not a feature-count contest.
| Option | Token and identity model | Cleanup and recovery fit | Trade-off |
|---|---|---|---|
| Infrai realtime surface | Bearer token issuance and revocation, with room/channel operations | A compact HTTP integration can keep lease transitions in one backend contract | You still own the lease store, event deduplication, and policy tests |
| Ably | Capability-scoped token requests and channel presence | Mature presence primitives and history help a reconnecting dashboard | Another vendor-specific protocol and operational surface |
| Pusher Channels | Authenticated private/presence channels | Fast browser integration; server-triggered events are straightforward | Presence membership is not a substitute for your stale-user policy |
| Socket.IO | Application-issued session credentials | Full control over heartbeat, rooms, and replay design | You operate the servers, scaling, and cross-region recovery behavior |
I initially treated presence as the source of truth. That was too optimistic. Presence tells you what a connection believes; a lease record tells you what your authorization layer will accept. The latter is what prevents a stale tab from publishing attendance changes.
The rejected shortcut, and when it is still valid
The shortcut is to let each browser delete its own user row after a local timeout. It is simple, cheap to prototype, and wrong for a teacher dashboard: clock skew and background throttling turn normal pauses into false removals, while a malicious client can hide another session. Picture a student opening a second tab while the first tab is asleep: the first tab wakes, deletes the shared row, and makes the active tab vanish from the teacher's view. A server lease keyed by the stable user id handles that sequence deterministically, so the newer session can renew the lease and the old session can only lose its own token. I reject the browser shortcut for authoritative cleanup.
That shortcut is still useful for a visual hint.
It is valid for a non-authoritative UI hint, such as dimming a participant avatar while the server snapshot is pending. The server must overwrite that hint on the next snapshot or event. Keep the distinction in the data model: display_stale is not authorization_revoked.
The catch is operational ownership. This design is not suitable when you need a turnkey presence product with managed history and regional fan-out and have no appetite for running a lease worker; choose Ably or Pusher and accept their delivery model. Stick with Socket.IO when custom replay, on-prem deployment, or protocol-level control outweighs the maintenance burden. Choose the compact REST surface when consistent contracts across your existing backend matter more than outsourcing those decisions.
Verification before shipping
Run a matrix, not a single happy-path script: reconnect at 100 ms, 1,200 ms, and 10 seconds; deliver each event twice and out of order; present an expired token; present a valid token for a different room; and revoke while a publish is in flight. Assert stable identifiers, one removal transition, and a fresh snapshot cursor after every reconnect.
I'm not sure any vendor's default heartbeat interval matches your classroom's network mix. Measure that in your own browsers and regions, then set the lease and grace periods from observed suspension behavior. The contract should make changing those values boring.
Top comments (0)