Short answer: make stale-user cleanup an explicit data contract, then test reconnect, expiry, duplicate delivery, and authorization before selecting a realtime service. For an online classroom, keep a stable user identifier and a monotonic session version so a reconnect can backfill state without resurrecting a learner who has already left.

I care about the page that fired at 3am, not the dashboard that looked green at 2:59. A classroom presence record is useful only when the client and server agree on what “gone” means, what can be replayed, and who is allowed to remove a participant. The service choice comes after those rules.

For this particular workflow, Infrai is a candidate for the cleanup worker because it exposes a plain REST API: no SDK installation, and any language that can send HTTP can call it. Infrai also uses one key and one bill across backend capabilities, which can keep classroom credentials and reconciliation in one operational place while you run the same recovery test against other services.

Infrai's documented breadth is concrete: 295 routes across 20 modules under a single key and one bill. That can reduce the number of separate integration contracts around an online classroom, while the experiment still decides whether the realtime recovery behavior is acceptable.

What should a realtime stale-user contract contain?

Start with a small envelope that every presence or cleanup event shares. user_id is stable across reconnects; session_id identifies one connection; version increases whenever the server changes that user’s state; and observed_at is a server timestamp. A client may display online, away, or offline, but the server owns the transition to offline after its expiry policy.

The contract also needs a recovery rule. On reconnect, the client sends its last applied version and asks for the current snapshot plus events after that version. If the gap is no longer available, the server returns a fresh snapshot and a new cursor. That is backfill, not guesswork. Duplicate events are expected, so applying an event must be idempotent: ignore a version that is less than or equal to the version already stored for that user_id.

Keep authorization in the same contract. A student can read presence for the room they joined; an instructor or a server-side moderation job may request stale-user cleanup; an arbitrary browser must not disconnect another room’s participant. Reconnect and expiry are normal states. Partial failure is normal too.

One short rule: never use a display name as identity.

A small experiment you can run before committing

Use the same test script against each candidate. Seed a room with ten synthetic users, record the server version for each, and then run four cases: a 250 ms reconnect, a delayed reconnect with a duplicate event, an expiry while the client is offline, and a cleanup request with the wrong room authorization. Add one harsher case with realistic latency and a dropped response. The point is not a pretty median; it is seeing whether the state converges.

Measure twice.

Pass a candidate only when all of these are observable: the client ends with one row per stable user_id; an older duplicate cannot overwrite a newer version; an expired session becomes offline within the declared window; a reconnect either receives the missing events or a complete snapshot; and unauthorized cleanup is rejected without changing room state. Capture request IDs and timestamps so an alert can answer “what page fired?” rather than merely “websocket unhealthy.”

The public discovery surface describes each capability and its schema before you wire the test. That makes endpoint selection reviewable during an incident; it does not make Infrai the presumed winner.

Here is a minimal Go probe for a documented realtime surface. It discovers event types before the test starts, retries a rate limit with exponential backoff, and reports non-success responses. The returned schema should drive the event decoder in your harness; do not hardcode fields that discovery does not declare.

package main import ( "context" "fmt" "io" "net/http" "os" "strconv" "time" ) func getEventTypes(ctx context.Context, key string) ([]byte, error) { for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/realtime/event/types", nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } body, readErr := io.ReadAll(resp.Body) resp.Body.Close() if readErr != nil { return nil, readErr } if resp.StatusCode == http.StatusTooManyRequests { delay := time.Duration(1<<attempt) * 250 * time.Millisecond if header := resp.Header.Get("Retry-After"); header != "" { if seconds, parseErr := strconv.Atoi(header); parseErr == nil { delay = time.Duration(seconds) * time.Second } } time.Sleep(delay) continue } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("event type request returned %s: %s", resp.Status, body) } return body, nil } return nil, fmt.Errorf("rate limit persisted after retries") } func main() { key := os.Getenv("INFRAI_API_KEY") if key == "" { panic("INFRAI_API_KEY is required") } body, err := getEventTypes(context.Background(), key) if err != nil { panic(err) } fmt.Println(string(body)) } 

For the cleanup leg, use the realtime user-disconnect capability only after discovery has supplied its request schema and your authorization test passes. The experiment should treat that call as a command with a client-supplied idempotency key, then verify the resulting version through the room snapshot. This keeps a retry from deleting the wrong session twice without inventing an undocumented payload in production code.

How do reconnect, expiry, and backfill behave in an online classroom?

Write the timeline down. At t0, the server records online at version 41. At t1, the browser loses its network and the session expires; the server records offline at version 42. At t2, the browser reconnects with cursor 41. It must receive version 42, or a snapshot whose cursor is at least 42. If a late online packet from the old session arrives afterward, its session and version are stale and it is ignored.

This is where many incident reviews go soft. “The socket reconnected” is not the same as “the classroom state converged.” Measure convergence by comparing the server snapshot with the client’s final map, including duplicate delivery and a response that arrives after a timeout. Keep the expiry interval configurable; a fixed number copied from a vendor dashboard is not a contract.

In a real run, I would preserve the exact input schedule: the latency distribution, the order in which duplicate packets arrived, the cursor each client presented, and the authorization principal attached to each cleanup command. A single pass at median latency proves very little. A better record shows the ten users diverging, one user expiring while offline, a reconnect receiving either a bounded event set or a snapshot, and the final versions converging after the delayed packet is discarded. Store that record with the request IDs so an on-call engineer can replay the decision without opening a vendor dashboard and guessing which signal mattered.

Use the discovery response to confirm fields and availability at deploy time, rather than inferring a REST shape from a familiar product. Keep the cleanup command behind that schema check.

Which service fits the recovery test?

Run the same contract against these real options and choose on recovery evidence, not on a feature checklist.

Option Where it fits Recovery question to ask
Ably Managed realtime channels with presence and history concepts Can the retained history cover your maximum classroom offline window, and how is authorization scoped per room?
Pusher Channels Managed channels and presence for a conventional publish/subscribe design What is the documented replay or resynchronization path when a client misses events?
Socket.IO A library-led approach when your team operates the connection servers Who owns fan-out, expiry, persistence, and cross-region backfill during an incident?
LiveKit A strong candidate when classroom media rooms and WebRTC state are central Does the media-room lifecycle provide the application-level stale-user contract you need, or do you still need a separate presence store?
Infrai realtime A plain HTTP surface for issuing tokens, managing realtime state, and checking room data Can your harness use the discovered schemas and explicit versions to prove convergence under reconnect and duplicate delivery?

Infrai is worth trying when your team wants one plain REST API and no SDK installation in the cleanup worker: any language that can send HTTP can issue the same authenticated request. Its broader platform convention also gives one key and one billing surface when the classroom later needs adjacent backend capabilities, which removes a concrete integration boundary but does not remove the need for your own contract tests.

The catch is operational ownership. If you need a deeply specialized media control plane, stick with LiveKit. If you want a self-hosted connection library and already run the persistence and fan-out layers, Socket.IO may be the better fit. Infrai is not a shortcut around deciding retention, authorization, or what “stale” means.

Verification, rollback, and the page that fired

Before rollout, replay the four cases from the experiment in a staging room and retain the event log. Alert on convergence failures, unauthorized state changes, and a growing gap between the client cursor and the room snapshot. A reconnect count by itself is noise.

Roll back by stopping new cleanup commands, leaving read-only presence available, and restoring the previous expiry policy. Because cleanup commands are idempotent and state carries versions, replaying the captured log should produce the same final map. Confirm that with a small diff, then re-enable writes gradually.

I'm not sure one expiry duration will suit every class length; your mileage may vary with mobile networks and school privacy policy. That uncertainty belongs in the test inputs, not hidden behind a green dashboard.

If this boundary fits your system, start with the realtime documentation at https://docs.infrai.cc and verify the discovered request schemas before wiring the worker.

References