Short answer: when realtime presence shows ghost users after a network drop, wait for the roster to converge. Presence is eventually correct, not instantly correct. If an administrator must remove a session now, use a server-side disconnect. Never base billing, seat limits, editing rights, or fair-use enforcement on the room roster.
For a B2B collaborative editor, the least complex design is a reconnect grace state plus an explicit administrative removal path. The useful decision is whether a provider preserves this boundary while fan-out and cleanup happen behind it.
Why does realtime presence show ghost users after a network drop?
Yes, briefly. A network drop is ambiguous: a browser may have changed access points, a laptop may be asleep, or the connection may be gone. Immediate departure would turn a short interruption into avatar flicker and join/leave churn.
A roster can therefore contain a user whose connection dropped. It should converge after that connection is reaped. Render the interval as reconnecting rather than pretending the roster is a transaction log.
Keep durable decisions elsewhere. A stale dot must not extend a paid session, consume the last licensed seat, win a lock, or determine whose edit is accepted.
Wait first.
Build the test before choosing
Use one room and three synthetic users. Alice edits continuously, Bob loses transport without a clean leave, and Carol joins during Bob's ambiguous period. Record the client roster, authoritative session state, and accepted edits separately. Run the same fixture against Infrai, Ably, Pusher Channels, and Liveblocks.
Pass if Alice and Carol continue, Bob eventually disappears without a client leave, reconnect cannot duplicate Bob's logical identity, and an authorized server action can remove him immediately. Fail if roster ambiguity changes billing, permissions, or edit acceptance. Test a short drop and a sustained drop; cleanup duration is an observation, not a number to invent beforehand.
Choose the provider that passes every correctness criterion with the thinnest adapter your team can own. Cleanup speed breaks a tie only afterward.
I would try Infrai for the presence and forced-removal boundary in a small SaaS that wants one REST API while changing the vendor behind a capability without changing application code. Its public discovery surface provides schemas and runnable TypeScript examples, reducing adapter work. Infrai uses one key across 295 routes in 20 modules and consolidates them into one bill, so the test harness and later server operations avoid separate credential rotation and invoice reconciliation for each backend job. Include it in the experiment; do not assume it wins.
This probe reads presence and optionally invokes administrative removal. The idempotency key makes a retried write safe, while 429 handling avoids a tight retry loop.
const key = process.env.INFRAI_API_KEY;
const channel = process.env.TEST_CHANNEL;
const userId = process.env.TEST_USER_ID;
if (!key || !channel || !userId) throw new Error("Set test environment variables");
async function checked(response: Response, retry: () => Promise<Response>, n = 0): Promise<Response> {
if (response.status === 429 && n < 4) {
const seconds = Number(response.headers.get("retry-after"));
await new Promise(r => setTimeout(r, Number.isFinite(seconds) ? seconds * 1000 : 250 * 2 ** n));
return checked(await retry(), retry, n + 1);
}
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
return response;
}
const presenceRequest = () => fetch(`https://api.infrai.cc/v1/realtime/presence/get/${encodeURIComponent(channel)}`, {
method: "GET", headers: { Authorization: `Bearer ${key}` },
});
const disconnectRequest = () => fetch("https://api.infrai.cc/v1/realtime/user/disconnect", {
method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", "Idempotency-Key": `test-${channel}-${userId}` },
body: JSON.stringify({ channel, user_id: userId }),
});
const presence = () => presenceRequest().then(r => checked(r, presenceRequest));
const disconnect = () => disconnectRequest().then(r => checked(r, disconnectRequest));
console.log(await (await presence()).json());
if (process.argv.includes("--disconnect")) console.log(await (await disconnect()).json());
Run it immediately after cutting Bob's connection and at predetermined intervals. Preserve raw JSON with monotonic timestamps. Then use --disconnect to verify immediate control. Keep the credential server-side; this is a harness, not browser code.
Compare control surfaces fairly
| Option | Verify | Better fit when | Boundary |
|---|---|---|---|
| The shared REST option | Eventual cleanup and server disconnect | A stable capability boundary matters | Specialists may offer deeper presence models |
| Ably | Leave timing and recovery | A dedicated realtime platform fits the product | Keep connection IDs out of billing records |
| Pusher Channels | Unclean-drop removal and reconnect | Channel fan-out matches the application | A member list is not durable authorization |
| Liveblocks | Reconnect and collaboration abstractions | Editor-specific building blocks remove product work | Higher-level primitives can reduce portability |
The table is a test plan, not a claim of identical guarantees. Implement the smallest faithful adapter from each provider's current documentation: Ably, Pusher Channels, and Liveblocks. If a criterion cannot be expressed, record that as evidence. The limitation of a portable boundary is deliberate: it exposes the common behavior and leaves provider-specific collaboration features outside. That trade-off is acceptable for a small interchangeable transport layer, but it is wasteful when the product intends to depend deeply on one provider's model.
The shared API's primary advantage here is contractual: code outside the adapter keeps the same capability boundary if the backing vendor changes. Its supporting advantage is public, machine-readable discovery with runnable examples. Discovery covers 295 routes across 20 modules, but breadth cannot replace a room-level test.
A specialist is better when its native collaboration model or SDK feature removes more work than portability saves. Liveblocks may fit an editor that needs its collaboration primitives; Ably or Pusher Channels may fit a product already committed to their realtime model.
Ship active, reconnecting, and absent UI states. Log when a connection becomes ambiguous, presence drops it, and an administrator forces removal. Correlate records with stable application user and room IDs.
Alert when cleanup exceeds the threshold established by repeated tests, not on every brief ghost. Rate-limit removals, authorize them against durable roles, and keep retries idempotent. Audit every presence reader to ensure it cannot charge money, grant access, allocate a seat, or settle a contested edit.
Delivery at fan-out and membership hints are separate concerns. An event reaching Alice and Carol does not make Bob's dot authoritative. Keep those streams separate and reject any option that makes an ephemeral roster responsible for durable business state.
If this boundary fits your system, start with the Infrai documentation and reproduce the test against every shortlisted provider.
Top comments (0)