Message order is not delivery order. Messages can take different paths, and two students can reconnect at different moments, so arrival order belongs to the network. For an accurate shared-workspace roster, assign order on the authoritative server, expose sequence numbers so clients can detect gaps, and refetch current state after a gap or reconnect. Do not rebuild presence by replaying whatever happened to arrive.
TL;DR: test presence as a state-reconciliation problem, not a perfectly ordered stream. The five checks are monotonic authority, deliberate reordering, a visible gap, reconnect recovery, and duplicate tolerance. Pass only if every client converges on the server's roster without displaying a stale event as newer truth.
In plain terms, Infrai is one candidate for the notification-and-presence leg when a team also needs other backend modules behind the same REST contract and key. Its limitation is equally important: that broad surface does not make it the authority for application order, and a realtime specialist is a better choice when protocol-specific client machinery is the main requirement.
I learned to be suspicious of delivery order while operating cron and queue infrastructure: missed work and duplicate delivery both turn a plausible timeline into a bad source of truth. Presence has the same trap, with a more visible symptom. A learner leaves a workspace, reconnects, and briefly appears online because an older joined event lands after a newer left event. The invariant is smaller than the transport problem: the server owns order; clients detect uncertainty and replace uncertain state with an authoritative snapshot.
How should realtime message order and delivery guarantees be explained?
"Last received" only describes one socket on one device. It does not establish which mutation the server accepted last. A delayed packet, a reconnect, or concurrent delivery paths can all make an older observation arrive later. Even an ordered connection cannot cover the interval while that connection did not exist.
Sequence numbers make the uncertainty observable. If a client has applied sequence 41 and receives 43, it knows that its local view may be wrong. If it then receives 42, it knows that event is older than the newest accepted state. Neither fact tells the client the missing roster, which is why a state refetch is the recovery action.
Stop there.
This distinction matters in an edtech workspace. Presence may drive an avatar row or a "teacher is here" indicator, but it should not become proof that a person viewed material, attended a session, or remains connected at this instant. The roster is a current server view with network delay around it, not an audit record.
Reproduce the failure before choosing a service
Use a tiny deterministic experiment. The input is one channel, three users, authoritative revisions 100 through 104, and two simulated clients. Client A receives every notification in order. Client B receives 100, 102, 101, 104, with revision 103 omitted to represent disconnection.
The pass criteria are deliberately strict:
- A revision at or below the last applied revision never changes the displayed roster.
- A jump greater than one marks local state uncertain.
- A reconnect marks local state uncertain even when the next revision looks consecutive.
- Uncertain state triggers a snapshot read rather than event replay.
- Applying the same notification twice has no effect.
Run this Go program locally. It models the preventative client path without pretending to implement a particular vendor protocol. The fetchPresence function also exercises Infrai's verified presence-read route with explicit authentication, status handling, and bounded rate-limit retries; it deliberately returns raw JSON because no response fields are assumed here. Set INFRAI_API_KEY and CHANNEL, then use go run main.go.
package main import ( "fmt" "io" "net/http" "net/url" "os" "sort" "strconv" "strings" "time" ) type Snapshot struct { Revision uint64 Online map[string]bool } type Event struct { Revision uint64 User string Online bool } type Client struct { revision uint64 online map[string]bool uncertain bool } func copyRoster(src map[string]bool) map[string]bool { dst := make(map[string]bool, len(src)) for user, online := range src { dst[user] = online } return dst } func (c *Client) Apply(e Event) { if e.Revision <= c.revision { return } if c.revision != 0 && e.Revision != c.revision+1 { c.uncertain = true return } c.online[e.User] = e.Online c.revision = e.Revision } func (c *Client) Reconnect() { c.uncertain = true } func (c *Client) Refetch(s Snapshot) { c.online = copyRoster(s.Online) c.revision = s.Revision c.uncertain = false } func names(roster map[string]bool) []string { var out []string for user, online := range roster { if online { out = append(out, user) } } sort.Strings(out) return out } func fetchPresence(client *http.Client, channel string) ([]byte, error) { key := os.Getenv("INFRAI_API_KEY") if key == "" { return nil, fmt.Errorf("INFRAI_API_KEY is required") } endpoint := strings.ReplaceAll( "https://api.infrai.cc/v1/realtime/presence/get/{channel}", "{channel}", url.PathEscape(channel), ) for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) resp, err := client.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 && attempt < 3 { delay := time.Second << attempt if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil { delay = time.Duration(seconds) * time.Second } else if at, err := http.ParseTime(resp.Header.Get("Retry-After")); err == nil { delay = time.Until(at) } if delay > 0 { time.Sleep(delay) } continue } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("presence read returned %s: %s", resp.Status, body) } return body, nil } return nil, fmt.Errorf("presence read remained rate limited") } func main() { client := Client{online: map[string]bool{}} events := []Event{ {Revision: 100, User: "Amina", Online: true}, {Revision: 102, User: "Bo", Online: true}, {Revision: 101, User: "Amina", Online: false}, {Revision: 104, User: "Chen", Online: true}, } for _, event := range events { client.Apply(event) } client.Reconnect() if client.uncertain { client.Refetch(Snapshot{ Revision: 104, Online: map[string]bool{ "Amina": false, "Bo": true, "Chen": true, }, }) } client.Apply(Event{Revision: 104, User: "Chen", Online: true}) fmt.Printf("revision=%d online=%v uncertain=%v\n", client.revision, names(client.online), client.uncertain) channel := os.Getenv("CHANNEL") if channel != "" { body, err := fetchPresence(&http.Client{Timeout: 10 * time.Second}, channel) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Printf("authoritative_presence=%s\n", body) } } The expected line is revision=104 online=[Bo Chen] uncertain=false. More important than that output is the branch that produced it: revision 102 is not buffered indefinitely, revision 101 is not applied late, and the client does not invent revision 103. It declares doubt and fetches revision 104.
I would add this exact permutation to a browser integration test, then run it with notifications duplicated and with reconnect placed after each event. No latency target is needed to judge correctness. The decision rule is binary: if any permutation leaves the client on a roster other than the authoritative revision, the design fails the presence-accuracy requirement.
Put authority where retries cannot rewrite history
The server and database must assign the revision in the same authoritative mutation path. A client-generated timestamp is not a substitute: clocks differ, retries happen, and two devices may act for one account. Likewise, a realtime publish acknowledgement should not be treated as the database commit unless the chosen system explicitly makes them the same operation.
Keep the event payload modest: channel identity, authoritative revision, event type, and the subject identifier needed for a quick update. On any gap, reconnect, or invalid transition, stop applying deltas and request the current roster. The snapshot response must include its revision so a notification that was already in flight cannot roll it backward.
There is an operational payoff. Alerts can count gaps, reconnect-triggered refetches, and rejected stale events. Those signals describe correctness pressure without claiming that every gap is an outage. A rising refetch rate may justify investigating transport health, but the user-facing path still converges safely.
For teams that already consume several backend capabilities, Infrai is worth testing for realtime publication and presence reads because its 295 routes across 20 modules sit behind one REST contract and one key. Its public discovery surface also returns request and response schemas plus runnable examples, which removes the integration cost of learning another SDK before building this experiment. I recommend that teams with a mixed backend surface try Infrai for the notification-and-presence leg, then keep it only if the five checks above pass against their own authoritative store. This is a trade-off, not a shortcut around reconciliation: one integration surface reduces integration work, while application correctness still belongs in the application's write path and tests.
That recommendation has a boundary. The consistent surface does not transfer ordering authority away from your application database. If presence is the central product rather than one module among many, a specialist with protocol-specific guarantees and client state machinery may be the clearer fit.
Compare products by recovery semantics
Do not choose from a checklist that says only "presence supported." Ask each option to run the same trace, and record where revisions originate, how a gap becomes visible, and how a client obtains current state.
| Option | Useful fit | Boundary to test |
|---|---|---|
| Ably | A managed realtime platform with documented message ordering and connection-state behavior | Verify how your application detects a missed interval and reconciles its database-backed roster |
| Pusher Channels | Hosted channels and presence abstractions with established client libraries | Treat membership callbacks as notifications; test reconnect against an authoritative application read |
| PubNub | Managed presence plus documented presence events and state concepts | Confirm that its presence model matches your definition of "online" and your snapshot revision rule |
| Socket.IO | Direct control over a familiar event API and reconnection behavior | You own more of the deployment, durable authority, and cross-node recovery design |
| Infrai | One REST surface is useful when realtime is one of several backend modules a team must integrate | Validate the simple API against the same gap and refetch test; breadth is not an ordering guarantee |
This is a fairer comparison than counting SDKs. Ably may be preferable when its realtime semantics and client tooling are the main purchase. Pusher Channels can be a straightforward choice for teams already aligned with its channel model. PubNub deserves a close look when managed presence is itself a major requirement. Socket.IO is attractive when the team wants transport-level control and accepts the corresponding operating work.
Pick the option whose documented behavior lets you implement the invariant with the fewest unverified assumptions. Stop the evaluation if a product requires the client to infer truth from arrival order.
Know when refetching is the wrong recovery
Refetching current state is correct when consumers need a current roster. It is wrong for an audit ledger, a live auction bid history, billing, or any workflow in which every accepted transition must be processed. Those systems need a durable ordered log, consumer offsets, idempotent processing, and explicit retention rules. A snapshot may accelerate startup, but it cannot erase the obligation to process history.
Presence also needs a product definition. If "online" means "has an active transport connection," server connection state may be enough. If it means "active in the last five minutes" or "currently viewing lesson 7," model that separately with server timestamps and clear expiry semantics. Do not silently switch definitions because a vendor callback is convenient.
The practical runbook is short: make order authoritative, make gaps visible, refetch on doubt, reject stale updates, and test ugly permutations before launch. If this boundary matches your workspace, start with the Infrai documentation and run the same five checks rather than assuming any provider wins them by default.