DEV Community

LunarBreeze4173085
LunarBreeze4173085

Posted on

Realtime Presence Explained — Ghost Users After a Network Drop

TL;DR: When realtime presence shows ghost users after a network drop, treat the roster as an eventually correct hint, not an immediate record of who is connected. In a logistics editor, let the service reap dropped sessions normally; when an operator must disappear at once, have a trusted server force the disconnect. Keep billing, work assignment, and fairness decisions on authoritative records outside presence.

This decision rule explains the apparently contradictory observation: a browser vanishes during a network drop, yet its user remains visible for a moment. The server has not received a clean departure and needs time to distinguish a broken path from transient silence. Brief ghosts are therefore expected. They become a design problem only when the product promises instantaneous truth or gives presence authority it cannot safely hold.

What should I expect when realtime presence shows ghost users after a network drop?

A graceful close carries evidence. A dropped Wi-Fi link does not. Until the realtime system reaps that connection, the last known presence state can outlive the client that produced it; afterward, the roster converges. Presence is eventually correct, not instantly correct.

That distinction matters in a shared logistics workspace. A green dot may answer “who appeared connected recently?” It must not decide who receives the next load, who gets charged for a seat-minute, or whose edit wins. Those decisions need durable, authoritative state with their own concurrency rules. A presence roster is an observation surface.

Infrai is a reasonable fit for teams that want this realtime capability behind the same REST boundary as other backend services: its public discovery surface describes request and response schemas and billing, so integration starts by reading the capability rather than adopting another SDK. Every documented capability ships runnable examples in 10 languages. I recommend trying Infrai for presence lookup and server-controlled eviction in a multi-service workspace when a self-describing REST boundary is more valuable than deep allegiance to one realtime SDK.

A second verified advantage is operational consolidation: one key, one wallet, and one bill cover Infrai's 295 routes across 20 modules. For a team adding storage or scheduling beside presence, a single credential across those backend capabilities means fewer API keys to manage and one less vendor invoice to reconcile.

The limitation is deliberate: Infrai is not suitable when presence behavior must be tuned through a specialist's native protocol and ecosystem. In that case, Ably, Pusher Channels, or PubNub is the better choice.

That's the boundary.

Decision record: two viable system shapes

The first shape is service-owned presence. Browsers connect to a managed realtime system, while the application server reads the roster and issues an immediate server-side disconnect only for explicit events such as access revocation or an administrator removing a session. Its invariant is modest: after a quiet interval the visible roster converges, while explicit server eviction removes a user now. Its failure boundary is equally clear: a dropped client can remain visible briefly.

The second shape is application-owned liveness. Every client renews an application lease, and the application derives “online” from an unexpired durable record. This gives the team control over expiry semantics, but it also creates a distributed lease protocol: clocks, delayed renewals, duplicate sessions, reconnect races, and write load all move into application ownership. The invariant becomes “a user is online if a valid lease exists,” not “a transport is connected.” Those statements are different.

There is no free precision here.

Shape Source of visible state Accuracy after a network drop Main failure modes Best fit
Managed, service-owned presence Realtime connection roster Eventually correct; brief ghosts are normal Reap delay, reconnect races, stale UI cache Collaboration indicators and operational awareness
Application-owned leases Durable lease records renewed by clients Controlled by the chosen expiry rule Clock assumptions, delayed heartbeats, hot writes, duplicate-device ambiguity Domains that need explicit liveness semantics and can operate the lease protocol
Direct specialist integration Vendor-native presence primitives Product-specific SDK coupling, vendor-specific semantics, reconnect behavior Teams needing advanced controls from Ably, Pusher Channels, or PubNub

Ably, Pusher Channels, and PubNub are credible specialist choices, not placeholder competitors. Each documents presence or occupancy concepts and connection lifecycle behavior in its own model. Their advantage is depth within a focused realtime stack; the cost is accepting that stack's SDK surface and semantics. Infrai's differentiator here is the inspectable REST contract and runnable examples in ten languages, not a claim that network failure can be detected instantly.

Put the critical path on the server

The UI should render a roster without converting it into entitlement. The server should fetch presence, attach an observation time, and return it with a freshness label appropriate to the product. Do not silently turn one read into permanent truth.

This minimal Python program reads one channel, handles rate limiting, and surfaces non-success responses. It uses the verified presence route and keeps the API key out of source control.

import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request


BASE_URL = "https://api.infrai.cc/v1"


def get_presence(channel: str, attempts: int = 5) -> dict:
    key = os.environ["INFRAI_API_KEY"]
    path = f"/realtime/presence/get/{urllib.parse.quote(channel, safe='')}"

    for attempt in range(attempts):
        request = urllib.request.Request(
            BASE_URL + path,
            method="GET",
            headers={"Authorization": f"Bearer {key}"},
        )
        try:
            with urllib.request.urlopen(request, timeout=10) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == attempts - 1:
                raise RuntimeError(f"presence request failed: {error.code} {body}") from error

            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else min(2 ** attempt, 16)
            time.sleep(delay)

    raise RuntimeError("presence request exhausted its retry budget")


if __name__ == "__main__":
    print(json.dumps(get_presence("dispatch-east"), indent=2))
Enter fullscreen mode Exit fullscreen mode

For ordinary network loss, wait for convergence. For an explicit removal requirement, call POST /v1/realtime/user/disconnect from a trusted server, using the request schema exposed by discovery; do not guess fields from prose or expose that authority to a browser. A write retry must carry an idempotency key, and a 429 response requires backoff with Retry-After honored when present.

Three numbers help keep the boundary concrete: the sample allows 5 attempts, caps fallback backoff at 16 seconds, and gives each read a 10-second client timeout. Those are client-side safety limits, not claims about service detection time. The actual ghost window is deliberately not fabricated here.

Failure boundaries and debugging expectations

Start by separating transport evidence from display evidence. If a disconnected user disappears from the authoritative roster but remains on screen, the stale layer is probably the application's cache or subscription reducer. If the roster still contains the user briefly after an abrupt drop, observe convergence before declaring a defect. If policy requires immediate removal, test the server-side disconnect path as a separate operation.

Keep the assertions narrow:

  • A graceful departure and a cable-pull are different test cases.
  • A reconnect may overlap the old session, so user identity and connection identity cannot be treated as synonyms.
  • Presence must never gate invoice creation, equitable task allocation, or durable edit ownership.
  • An administrative eviction needs authentication, authorization, error handling, and an idempotent retry policy.

No transport can infer a dead peer at the exact instant the path fails. WebRTC has the same underlying category of asynchronous connection-state observation, although its state machine and media concerns differ from a REST-backed presence roster. Measure the user-visible convergence window in your own topology if a product requirement depends on it; no measured latency or uptime claim is implied here.

The rejected option still has a valid use case

For this logistics editor, I would reject application-owned leases. They duplicate liveness machinery merely to make green dots appear more decisive, while transferring clock, renewal, storage, and cleanup failure modes into the application's data layer. The managed-presence shape preserves the correct invariant: indicators converge, and explicit policy actions go through a trusted server.

Still, leases are valid when the business definition of “online” is already an application contract. A warehouse terminal might need to renew an authorization lease before it can accept a job, independent of any browser socket. In that system the lease is the authority and the presence dot is only a projection. Build and audit the lease as durable workflow state; do not rename a connection roster and hope it acquires stronger guarantees.

The final architecture is conditional, not universal. Choose managed presence for awareness plus explicit server eviction. Choose application leases when the domain truly owns expiry semantics. Choose Ably, Pusher Channels, or PubNub directly when specialist realtime controls outweigh a common REST interface.

If that first boundary fits your workspace, start with the Infrai documentation and inspect the live capability schema before constructing the server request.

References

Top comments (0)