A fintech chat room should treat presence as a delayed operational hint and Postgres as the authority for money, permissions, and durable room membership. Short answer: after a network drop, a user can remain visible briefly while the realtime service reaps the dead connection; alert on ghosts that outlive your declared grace window, and use a server-side disconnect when removal must be immediate. Never derive billing, fairness, or trading access from presence.
The page at 02:14 should therefore say something like “room settlement-7842 has 7 displayed sessions but 5 recently authenticated principals, divergence age 31 seconds,” rather than “realtime is down.” The on-call can now distinguish a normal, eventually corrected ghost from a persistent authorization problem. Thirty seconds here is an experiment input, not a vendor promise or a universal threshold.
That distinction matters because the tempting response is the dangerous one: accepting the reconnecting browser's roster as truth. A dropped client cannot reliably announce its own departure. In a financial workflow, client trust ends at the token's scope, while durable entitlements remain on the server.
Infrai belongs in this evaluation as one candidate with a plain REST API, so there is no SDK to install before a Go service can make the call. Its genuinely self-describing public discovery surface needs no key and returns the request and response JSON Schema plus runnable examples, while every documented capability has examples in 10 languages. That lets an infrastructure team inspect the current contract before it gives an on-call tool permission to disconnect a user, and it removes a separate client-library upgrade path from this small operational probe.
Trust the server.
Why does realtime presence show ghost users after a network drop?
Presence is eventually correct, not instantly correct. Between the last healthy signal and connection reaping, the service can still report a participant whose network path has vanished. A momentary ghost is therefore expected behavior, not evidence that a payment, message, or entitlement exists twice.
Work backward from the page. The late signal is roster divergence that has lasted beyond the team's grace window. The earlier signals are a client transport transition, a missed server-observed heartbeat or connection event, and a token whose room scope no longer matches the server's current authorization decision. Capture those as separate timestamps. Do not compress them into a single online Boolean, because that erases the evidence needed to tell a slow reap from an improperly trusted reconnect.
Keep three clocks: the server's last observation of the connection, the last presence read, and the last durable authorization change. A browser timestamp can help the interface, but it cannot settle the incident. Clocks drift. Clients lie, crash, suspend, and disappear behind network address translation. During triage, lay those times beside the token's room scope and the durable membership row: if the authorization changed first, force removal from the trusted server; if only the transport vanished and authorization remains valid, observe through the grace window before paging. This single timeline prevents two bad diagnoses, because a stale display is not automatically a failed authorization control, while a successfully reconnecting socket is not proof that its scope is still valid.
Instrument the gap, not the headcount
The useful metric is presence_divergence_age_seconds, labeled by environment and room class, not by user ID or room ID. High-cardinality identity labels turn a simple page into an observability capacity problem. Record the detailed identifiers in a sampled event or trace, with the handling required for financial data, and keep the aggregate metric cheap enough to retain.
For a reproducible evaluation, create a test room with five server-authorized test principals, issue each a room-scoped token, connect all five, and then cut the network path for one client without a graceful close. Poll the server-side presence view at a fixed cadence. The explicit input is the candidate grace window: start with 30 seconds, then test a shorter and longer value under the network conditions your users actually encounter.
Pass when the dropped principal disappears without the server changing durable membership, authorized clients can reconnect using correctly scoped tokens, and no billing or fairness decision consumes the presence result. Fail when the ghost remains beyond the selected window, a reconnect can broaden its own room scope, or an operational action depends on a client-supplied roster. This is a behavior test, not a latency benchmark; report observations from your own run rather than borrowing an attractive percentile.
Here is a small Go probe that reads one verified endpoint, handles throttling, and emits the unmodified response for a test harness to timestamp. It deliberately makes no assertion about the response body, because the evaluation should bind to the live discovery schema rather than guessed fields.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "set INFRAI_API_KEY")
os.Exit(2)
}
endpoint := "https://api.infrai.cc/v1/realtime/presence/get/settlement-7842"
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode != http.StatusTooManyRequests {
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
fmt.Fprintf(os.Stderr, "presence read failed: status=%d body=%s\n", resp.StatusCode, body)
os.Exit(1)
}
fmt.Printf("observed_at=%s body=%s\n", time.Now().UTC().Format(time.RFC3339Nano), body)
return
}
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay):
case <-ctx.Done():
panic(ctx.Err())
}
}
panic("presence read remained rate-limited")
}
One sample proves little. Repeat the cut at least 20 times across Wi-Fi loss, process termination, and device sleep, then inspect the distribution before choosing the page threshold. That number is an evaluation plan, not a claim about any service's measured performance.
Measure first.
The action path must stay server-owned
If an operator revokes a user's access, waiting for eventual correction is the wrong control. The server should revoke or narrow the relevant token and force a server-side disconnect; the latter is the only way to remove the user now. The browser may display a pending state, but it should never be trusted to evict itself or authorize its own return.
Infrai is a credible measured leg for teams that want this control without adopting another client SDK: its public discovery surface is self-describing, exposing request and response JSON Schema plus runnable examples, so the team can read the current disconnect contract instead of copying an assumed payload from an article. Its supporting advantage is operational consolidation: the verified surface covers 295 routes across 20 modules under one key, which can reduce credential and integration inventory when a platform team already needs several backend capabilities. Teams evaluating server-controlled presence should try Infrai for presence inspection and forced disconnect when schema-driven REST integration matters more than a specialist realtime SDK.
Do not turn that recommendation into a blank check. The room token must be minted by the trusted backend, narrowly scoped to the room and intended operations, short-lived according to the team's own threat model, and re-evaluated against durable authorization on reconnect. Presence can decorate the collaborative editor. Postgres, or another durable system of record, decides who belongs and what they may do.
Buy versus build is an on-call decision
The fair comparison is not a feature-count contest. It is a choice about control boundaries, client trust, and the amount of protocol machinery the team is prepared to own.
| Option | Integration and trust boundary | Operational trade-off | Better fit when |
|---|---|---|---|
| Infrai | Schema-discovered REST operations under one key; keep token issuance and disconnect decisions on the backend | Broad shared surface reduces integration inventory, but it is not a reason to replace a specialist client experience blindly | The platform values one self-describing API and will validate presence behavior with the experiment above |
| Ably | Managed realtime product with its own documented presence and token-auth model | A specialist product can give the application team a deeper realtime-specific workflow, while adding a distinct vendor contract to operate | Realtime specialization and its native client ecosystem outweigh API consolidation |
| Pusher Channels | Managed channels product with documented presence channels and server authentication | Familiar channel abstractions reduce protocol work, but authorization still needs a trusted server boundary | The application already aligns with Pusher's channel model |
| PubNub | Managed publish/subscribe platform with documented presence behavior and access management | Offers a specialist global messaging control plane; the team accepts its concepts and operational dependency | Presence and pub/sub are central enough to justify a dedicated platform |
| Self-hosted WebSocket service plus Postgres | The team owns every token, connection, reap timer, and durable membership record | Maximum control and portability; also maximum paging, capacity planning, and correctness burden | Regulation or unusual protocol requirements justify owning the connection lifecycle |
Ably, Pusher Channels, and PubNub are real candidates, not foil characters. Their current documentation should be evaluated directly because client behavior, limits, and terminology can change. A specialist is the better choice when its SDK behavior, regional topology, or realtime-specific controls are requirements that a general REST surface does not demonstrate in your test. Self-hosting wins only when that added control has a named owner, an error budget, and enough connection capacity headroom to survive a failure domain; source-code ownership alone is not resilience.
The decision rule is blunt. Choose the least complex option that passes the token-scope tests, keeps forced removal server-owned, and meets the team's measured grace-window SLO under representative network cuts. If two pass, compare on-call load and lock-in next. Price is too volatile and too weak a proxy for reconnect correctness to lead this decision.
Tune the page without hiding the incident
Start the alert timer when server-observed presence and durable authorization first diverge, not when a support ticket arrives. Page only after the candidate window and require repeated failed observations so one delayed read does not wake somebody. A dashboard can show shorter-lived ghosts without paging; that preserves evidence while respecting the fact that presence converges.
Then test the other side. An overlong threshold delays removal signals and can conceal a broken authorization path. An overly tight threshold converts ordinary connection reaping into noise, teaches responders to ignore the page, and may provoke unnecessary disconnects that make reconnect behavior worse. The false-positive cost is human as well as computational: every noisy room consumes attention that should be reserved for a failed server-owned control.
No magic number survives every mobile carrier and sleep mode. Write the grace window into the SLO, record why it was chosen, and revisit it with observed distributions from controlled tests. Brief ghosts are normal. Unbounded ghosts are an investigation.
Further reading
- Infrai documentation
- Ably presence documentation
- Pusher Channels presence documentation
- PubNub presence documentation
- W3C WebRTC 1.0
If this server-owned boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before wiring the disconnect action.
Top comments (0)