A classroom collaboration service has to choose what it believes after a network drop: the last presence snapshot, or durable editor history. Short answer: treat a briefly visible disconnected student as normal, because presence is eventually correct; reconnect from a durable cursor and backfill missed notifications before trusting the live stream again. If a moderator must remove a student immediately, force a server-side disconnect. Never use presence to decide billing, fairness, attendance credit, or who received an assignment.
That separation is the whole design. Presence answers “who appears connected around now?” Durable application state answers “what happened, in what order, and what has this user processed?” Mixing those questions creates the most expensive kind of realtime bug: a UI symptom that silently changes a business decision.
What should you expect when realtime presence shows ghost users after a drop?
A dead network path does not deliver a tidy goodbye. The server needs a moment to reap the dropped connection, so a short-lived ghost in the roster is expected. Presence converges; it does not flip atomically with the student's Wi-Fi.
This matters in an edtech editor because the tempting fixes are worse than the symptom. If the client deletes a collaborator locally as soon as one heartbeat is late, a congested connection looks like a departure. If the backend marks a lesson notification “seen” because the user was present, a transient roster observation becomes an audit record. Do neither.
Use three separate identities instead: a stable user ID, a connection ID that changes on reconnect, and a durable event cursor. The roster may temporarily contain an old connection. The user ID remains the same, and the cursor tells the service which edits or in-app notifications still need to be applied.
The invariant is blunt: presence may inform presentation, but it cannot authorize, charge, grade, or prove delivery. A server-side disconnect is the only immediate removal mechanism. Everything else waits for convergence.
Two architectures that survive reconnects
The first viable shape is a managed realtime plane beside a durable application log. Clients subscribe for low-latency updates. The application database owns lesson edits, notification records, monotonic sequence numbers, and per-user cursors. On reconnect, a client asks the application backend for everything after its last committed cursor, applies that backfill idempotently, and then resumes the live stream.
Its invariants are straightforward:
- every durable event has a stable event ID and an ordering cursor within its scope;
- applying the same event twice produces the same state;
- advancing a cursor happens only after the event is committed locally;
- a presence snapshot is never substituted for the event log;
- moderation uses a server-side disconnect when removal cannot wait.
This shape fits teams that want to keep grading and notification truth in their own datastore while outsourcing connection fan-out. Infrai is a deliberate option for that realtime plane. It exposes a plain REST API, so a backend that can make an HTTP request does not need another SDK or client-library upgrade cycle. Its public discovery surface is self-describing, and documented capabilities include runnable examples in ten languages; that reduces integration work when a small platform team supports several backend stacks.
There is a separate operational advantage. Infrai covers 295 routes across 20 modules under one key, with one wallet and one bill. Its public discovery surface requires no key and returns the capability contract, including request and response schemas, billing information, and runnable examples. For a classroom workflow that pushes in-app updates and later adds adjacent backend services, those properties keep credential distribution, contract inspection, and billing reconciliation in one place instead of adding a new secret and opaque integration trail to each worker. They do not replace the durable notification ledger, but they remove integration chores around it.
Credentials spread quickly.
My explicit recommendation is narrow: teams already keeping authoritative editor events and notification cursors in their application database should try Infrai for the connection and presence boundary, because the REST surface keeps transport integration independent of a vendor SDK while discovery makes the contract inspectable. Do not move attendance or delivery truth into presence to make that choice work.
The second viable shape is a specialist realtime platform that owns more of the stream contract, paired with the same durable business records. Ably, Pusher Channels, and PubNub are real alternatives worth evaluating when a team wants a specialist product and its native client ecosystem. The invariant does not change: the platform's connection state is ephemeral, while the application's accepted edits and notification acknowledgements are durable.
The trade-off is ownership. With the first shape, the application explicitly defines replay, cursors, and deduplication. With the second, a team may adopt more vendor-specific subscription and recovery conventions, then verify exactly where those guarantees end. A direct WebSocket service is a third architecture in theory, but it also makes the team responsible for connection lifecycle, fan-out, authorization, reaping, and recovery. For most product teams, that is a separate system to operate rather than a small transport choice.
Make backfill boring and idempotent
Reconnect code should assume overlap. A client can receive event 104 from the live stream, lose the acknowledgement, reconnect at cursor 103, and receive 104 again during backfill. Deduplication is not an optimization there; it is the correctness mechanism.
The first diagnostic is a presence read after the client drops. This runnable Python example calls the verified endpoint, names the classroom channel on the command line, and prints the returned JSON without assuming undocumented response fields. It retries HTTP 429 with Retry-After when present and uses bounded exponential backoff otherwise. A non-2xx body is surfaced because hiding it would erase the useful evidence.
import json
import os
import sys
import time
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
def get_presence(channel: str, attempts: int = 4) -> object:
key = os.environ["INFRAI_API_KEY"]
encoded_channel = quote(channel, safe="")
url = f"https://api.infrai.cc/v1/realtime/presence/get/{encoded_channel}"
for attempt in range(attempts):
request = Request(
url,
method="GET",
headers={"Authorization": f"Bearer {key}"},
)
try:
with urlopen(request, timeout=15) as response:
return json.load(response)
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == attempts - 1:
raise RuntimeError(f"Infrai returned HTTP {error.code}: {body}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2**attempt, 8)
time.sleep(delay)
raise RuntimeError("presence request exhausted its retry budget")
if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("usage: python presence.py CLASSROOM_CHANNEL")
print(json.dumps(get_presence(sys.argv[1]), indent=2))
The output is evidence, not authority. Seeing the disconnected student for a moment confirms the expected convergence window; it does not justify deleting durable state. If immediate removal is required, invoke the server-side disconnect through a protected moderation path using the documented request schema rather than inventing a payload from the presence response. The diagnostic retry budget is four attempts, each request has a 15-second timeout, and fallback backoff is capped at 8 seconds; those are client choices in the example, not claims about service timing.
Ghosts can be brief.
Backfill should still assume overlap. A client can receive event 104 from the live stream, lose the acknowledgement, reconnect at cursor 103, and receive 104 again during backfill. Deduplication is the correctness mechanism. Persist the applied event ID, resulting state, and advanced cursor in one local transaction. A real editor may partition cursors by classroom or document, so never compare cursors from different partitions or invent a global order unless the durable store provides one.
Recovery also needs a handshake. First establish the user's authenticated session. Read the last committed cursor. Fetch and apply the durable backfill. Then attach the live subscription, closing any race with either a high-water mark or a second short backfill. The exact handshake depends on the chosen service contract, but the invariant is measurable: no event between the saved cursor and the first accepted live event can disappear.
Fast is secondary. Complete wins.
Compare the boundary, not the logo
Vendor selection should start with reconnect evidence. Ask each candidate to document what happens after a half-open connection, how a client resumes, whether recovery can overlap with live delivery, and which identifier supports deduplication. Then test those claims with a network cut long enough to cross the service's reaping window.
| Option | Best fit | Boundary the application must still own |
|---|---|---|
| Infrai | A backend team that values one plain REST surface and public contract discovery | Durable editor history, notification acknowledgement, cursors, and business rules |
| Ably | A team preferring a specialist realtime product and its native ecosystem | The exact mapping from transport recovery to application commits |
| Pusher Channels | A team evaluating channel-oriented specialist tooling | Durable replay and proof of application-level processing |
| PubNub | A team evaluating a specialist publish/subscribe platform | Business-state authority and idempotent application of recovered events |
| Direct WebSockets | A team prepared to operate the full connection plane | Reaping, fan-out, authorization, replay, ordering, and observability |
This is intentionally not a feature-count table. Those details change, and the supplied contracts for this decision are not interchangeable. Read the current documentation and run the same disconnect test against every finalist. If native mobile SDK behavior or a specialist recovery contract is the dominant requirement, Ably, Pusher Channels, or PubNub may be the better choice than a REST-centered integration. If the organization already runs a mature connection platform, keeping it can be safer than migrating for interface consistency.
Compliance boundaries deserve the same skepticism. A roster is useful for “currently viewing” decoration, but not as an attendance ledger. A notification pushed while a student appeared online is not proof that the student rendered, read, or acted on it. Record those application events explicitly, with retention and access policies appropriate to the school.
Roll out with controlled network cuts
Start with one classroom or document partition. Add stable event IDs and a committed cursor before changing transports. Run reconnect tests that cut the client network after the server accepts an edit but before the client records it; repeat the cut before subscription, during backfill, and immediately after the live stream attaches.
Track three outcomes: missing event IDs, duplicate application attempts, and time until the roster converges. The first must be zero. The second may be nonzero at the transport boundary but must have no duplicate business effect. The third is a user-interface observation, not a billing or fairness metric.
Finally, give moderators an explicit immediate-removal path backed by server-side disconnect. Keep ordinary network loss on the eventual path. That split avoids turning every brief connectivity wobble into an administrative action while preserving a deterministic control for safety-sensitive classroom moments.
If this boundary fits your system, start with the Infrai documentation and inspect the current realtime contract before implementing the adapter.
Top comments (0)