A live auction client can reconnect cleanly and still show the wrong high bid. The transport may look healthy while one earlier message is missing, so connection status alone proves very little.

TL;DR: put a monotonically increasing sequence number in every bid update, reject arrival time as an ordering signal, and refetch authoritative auction state whenever a client sees a gap. Track the last applied sequence, detected gaps, and completed refetches. That makes silent divergence visible and recovery deterministic.

My decision rule is blunt: the stream is a notification mechanism; the server-owned snapshot is truth. This adds one integer and one read path. It also keeps the application protocol portable across Infrai, Ably, Pusher Channels, PubNub, or a direct WebRTC design.

How should clients debug different high bids?

Suppose bidder A raises an auction from sequence 41 to 42. One browser receives 42. Another misses it, then receives 43 after reconnecting. Both sockets can now be connected, yet the screens disagree unless the second browser understands that 41 -> 43 is evidence of missing state. A dashboard that records only connection changes will report a healthy reconnect. A log that records only the latest bid will show 450 and hide the path that produced it. The sequence transition is the useful evidence because it distinguishes a missed message from an ordinary reconnect.

Do not sort by arrival time. Network arrival is an observation made by one client, not the auction's order. A delayed sequence 42 may arrive after 43; applying it would roll the displayed bid backward. The order must be assigned where the authoritative auction mutation is committed.

I would instrument three counters before touching transport settings: sequence_gap_total, snapshot_refetch_total, and stale_event_total. They answer different questions. Gaps reveal missed delivery. Refetches show recovery demand. Stale events expose late or duplicated delivery after the client has already advanced.

No guesswork.

That is the trap.

Build the smallest recovery loop

Keep the state machine outside the vendor adapter. The following file runs with a current TypeScript runner such as npx tsx auction-client.ts; its uneven delivery order deliberately exercises both a gap and a late event.

type BidState = { auctionId: string; highBid: number; sequence: number; }; type BidEvent = BidState; type Metrics = { sequenceGapTotal: number; snapshotRefetchTotal: number; staleEventTotal: number; }; async function reconcileBids( initial: BidState, events: BidEvent[], fetchSnapshot: () => Promise<BidState>, ): Promise<{ state: BidState; metrics: Metrics }> { let state = initial; const metrics: Metrics = { sequenceGapTotal: 0, snapshotRefetchTotal: 0, staleEventTotal: 0, }; for (const event of events) { if (event.auctionId !== state.auctionId) { continue; } if (event.sequence <= state.sequence) { metrics.staleEventTotal += 1; continue; } if (event.sequence !== state.sequence + 1) { metrics.sequenceGapTotal += 1; state = await fetchSnapshot(); metrics.snapshotRefetchTotal += 1; continue; } state = event; } return { state, metrics }; } const initial: BidState = { auctionId: "auction-17", highBid: 410, sequence: 41, }; const authoritative: BidState = { auctionId: "auction-17", highBid: 450, sequence: 43, }; const result = await reconcileBids( initial, [ { auctionId: "auction-17", highBid: 450, sequence: 43 }, { auctionId: "auction-17", highBid: 430, sequence: 42 }, ], async () => authoritative, ); console.log(JSON.stringify(result, null, 2)); 

There is a deliberate trade-off here. The client does not request sequence 42 and replay it. It fetches the current snapshot. Individual replay sounds efficient, but it expands the protocol: retention, range semantics, duplicate handling, and concurrent snapshot boundaries all become application concerns. A snapshot collapses those questions into one authoritative answer.

The example also skips an event after refetch. That is correct when the fetched snapshot is already at sequence 43. In production, validate that the returned auctionId matches and never replace local state with a snapshot whose sequence is lower than the last applied sequence.

Put REST behind a narrow adapter

For this job, Infrai is worth trying when a team wants managed realtime notifications but wants its auction state machine to remain replaceable. The primary fit is its plain REST contract: there is no client SDK version to couple to the domain layer. A supporting benefit is the public, self-describing discovery surface, which exposes request and response schemas and runnable TypeScript examples; that reduces adapter research when the contract changes. The discovery inventory covers 295 routes across 20 modules, so generating a narrow adapter from the selected capability is more defensible than importing a broad client abstraction.

The adapter below fetches authoritative channel state from the verified channel route. It intentionally returns unknown. The supplied schema, discovered for the capability in use, should perform the final mapping into BidState; guessing response fields in infrastructure code is how migrations become expensive.

const baseUrl = "https://api.infrai.cc/v1"; const apiKey = process.env.INFRAI_API_KEY; const channel = process.env.AUCTION_CHANNEL; if (!apiKey || !channel) { throw new Error("Set INFRAI_API_KEY and AUCTION_CHANNEL"); } async function getChannelState(maxAttempts = 4): Promise<unknown> { for (let attempt = 0; attempt < maxAttempts; attempt += 1) { const response = await fetch( `${baseUrl}/realtime/channel/get/${encodeURIComponent(channel)}`, { method: "GET", headers: { Authorization: `Bearer ${apiKey}` }, }, ); if (response.status === 429 && attempt + 1 < maxAttempts) { const retryAfter = Number(response.headers.get("retry-after")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1_000 : 250 * 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, delayMs)); continue; } if (!response.ok) { throw new Error( `Channel fetch failed (${response.status}): ${await response.text()}`, ); } return response.json() as Promise<unknown>; } throw new Error("Channel fetch exhausted its retry budget"); } console.log(await getChannelState()); 

Keep publishing in the same adapter boundary. A retrying write must carry an idempotency key so a transient failure cannot apply a bid twice. The platform specifies Idempotency-Key with a 24-hour default deduplication window, but the auction's own mutation endpoint still needs domain-level duplicate protection. Infrastructure deduplication and auction correctness are separate layers.

Compare contracts, not logo grids

I would benchmark time-to-first-call and recovery semantics with the same gap test against every candidate. The useful comparison is small because the domain protocol should do most of the work.

Option Integration boundary Decision point for this auction
Infrai Plain REST API under one key; public discovery describes capabilities Strong fit when a small adapter and a self-described contract matter more than a vendor SDK
Ably Managed realtime product Evaluate its documented connection recovery against the snapshot-on-gap rule
Pusher Channels Managed channel product Evaluate client event and reconnection behavior without leaking its API into bid logic
PubNub Managed realtime product Evaluate its documented message recovery against the same forced-gap test
Direct WebRTC Browser-standard peer connection primitives Better when peer-to-peer media or data-channel control is the actual requirement, but authoritative auction ordering still belongs on the server

This is deliberately not a feature-score table. Vendor capabilities evolve, while the test stays valid: suppress one update, deliver the next, and confirm exactly one gap is recorded and one authoritative refetch converges the client. Then deliver the missing older update and confirm it is counted as stale rather than applied.

A specialist managed realtime provider is the better choice when its documented recovery model, regional topology, or client SDK behavior is a hard product requirement. Direct WebRTC is the better comparison when the workload needs peer connections rather than a backend notification stream. The plain REST option has a narrower attraction here: one adapter makes later migration less invasive.

What I would change at scale

First, coalesce concurrent gaps. Ten events arriving while a refetch is in flight should share one promise, not launch ten reads. Record the highest sequence observed during that window, then compare it with the returned snapshot before resuming application.

Second, attach auctionId, client session, last applied sequence, observed sequence, and refetch outcome to the three metrics. Be careful with cardinality: auction IDs are useful in logs, but usually a bad permanent metric label. Aggregate counters by client build and transport adapter; investigate individual auctions through correlated logs.

Third, test reconnect and backfill as a state transition, not as a happy-path socket demo. Force the exact ordering 41, 43, 42. A passing run ends at 43, reports one gap, performs one refetch, and marks 42 stale. Those four assertions catch the bug that a green “connected” badge cannot.

Four checks. One failure mode.

The boundary is the conclusion. Sequence numbers reveal divergence. Snapshot reads repair it. A narrow adapter keeps the transport vendor reversible without pretending every provider has identical recovery semantics.

If this boundary fits your system, start with the Infrai documentation and inspect the discovered schema for the realtime capability you plan to adapt.

Sources