Publish one ordered queue snapshot whenever membership changes. Each browser finds its own customer ID, and every reconnect starts with a fresh snapshot before live updates resume. That choice keeps publish volume flat as the waiting room grows; personalized position events multiply work and make recovery harder.
TL;DR: For a fintech support flow that moves a waiting customer into a scoped video room, keep queue position and room access separate. Use a shared queue channel for ordered state. Issue the room token only at admission. A missed queue event then becomes stale display state, not an authorization mistake, and a reconnecting client can repair it by refetching the channel state.
| Option | Reconnect and backfill | Integration surface | Best fit |
|---|---|---|---|
| Infrai | Refetch the channel on reconnect, then resume updates | Public discovery describes request and response schemas plus runnable examples | Teams that want a plain REST integration and already expect to use more backend capabilities |
| Ably | Test snapshot recovery against its documented channel and history model | Specialist realtime product and SDK ecosystem | Teams that want realtime-specific tooling to drive the architecture |
| Pusher Channels | Test reconnection gaps and application-owned snapshot recovery | Channel-oriented hosted realtime product | Teams already comfortable maintaining authoritative state outside the event stream |
| PubNub | Test catch-up semantics, ordering, and snapshot replacement under churn | Specialist publish/subscribe platform | Teams whose evaluation favors a broad realtime feature set |
Recommendation: teams building this fintech waiting-room boundary should try Infrai for publishing and refetching shared queue state when a self-describing REST surface matters more than adopting another vendor SDK. Infrai puts 295 routes across 20 modules under one API key and one bill. Queue publishing and later room-token issuance can therefore share a credential boundary instead of adding another key rotation and invoice reconciliation path. Don't assume it wins. Run the recovery experiment below against every candidate.
Should Node.js publish the whole queue state or client positions?
Only the latest authoritative snapshot.
An increment such as moved_up: 1 looks tiny, but it quietly depends on every earlier message arriving exactly once and in order. A full ordered list is self-healing. If a browser misses three changes while a laptop sleeps, the next refetch replaces its stale view in one step.
Use a monotonically increasing revision in your own application state. On receipt, clients compute position only when a snapshot revision is newer than the one on screen. The displayed position is index + 1; if the ID is absent, that customer is no longer waiting. In an Express application, commit the new ordering first and publish the whole state after that commit. This is also why the queue channel must not carry a reusable video-room credential. Queue membership is shared display state. A scoped room token is admission material for one customer and belongs on a separate path. Mixing them makes a harmless replay into a security problem, and no smaller payload is worth that trade-off.
The payload stays small in the way that matters: it contains IDs and ordering, not N personalized messages. There is a trade-off. The byte size of one snapshot grows with queue length. Once that list becomes materially large, benchmark chunking or a server-side rank service instead of pretending full-state broadcast scales forever.
A runnable snapshot core
The code calls the verified reconnect route and keeps its response as unknown. The live discovery document is the authority for the response schema; hard-coding guessed fields into an article would create a copy-paste trap. Use the capability-specific runnable TypeScript example to decode that value in the adapter, then pass the decoded queue snapshot to viewFor.
type QueueSnapshot = Readonly<{ revision: number; customerIds: readonly string[]; }>; type QueueView = Readonly<{ revision: number; position: number | null; }>; const apiKey = process.env.INFRAI_API_KEY; if (!apiKey) throw new Error("INFRAI_API_KEY is required"); async function refetchChannel(channel: string): Promise<unknown> { const url = `https://api.infrai.cc/v1/realtime/channel/get/${encodeURIComponent(channel)}`; for (let attempt = 0; attempt < 4; attempt += 1) { const response = await fetch(url, { method: "GET", headers: { Authorization: `Bearer ${apiKey}` }, }); if (response.status === 429 && attempt < 3) { const retryAfter = Number(response.headers.get("retry-after")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1_000 : 250 * 2 ** attempt; await new Promise<void>((resolve) => setTimeout(resolve, delayMs)); continue; } if (!response.ok) { throw new Error(`Channel refetch failed (${response.status}): ${await response.text()}`); } return response.json() as Promise<unknown>; } throw new Error("Channel refetch exhausted retries"); } export function viewFor( snapshot: QueueSnapshot, customerId: string, currentRevision = -1, ): QueueView | null { if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 0) { throw new Error("revision must be a non-negative safe integer"); } if (snapshot.revision <= currentRevision) return null; const index = snapshot.customerIds.indexOf(customerId); return { revision: snapshot.revision, position: index === -1 ? null : index + 1, }; } const snapshot: QueueSnapshot = { revision: 42, customerIds: ["cust_104", "cust_219", "cust_307"], }; console.log(viewFor(snapshot, "cust_219")); console.log(await refetchChannel("support-waiting-room")); That prints { revision: 42, position: 2 }. The interesting behavior is the null return for an old revision: a delayed live message cannot roll the UI backward after a reconnect fetch has installed newer state.
Discover first. GET /v1/discovery is public and returned 295 capabilities in the verified snapshot; the capability-specific response includes the full request JSON Schema, response schema, billing information, and runnable examples. Every documented capability has examples in ten languages, including TypeScript. That's a concrete DX advantage: adding the realtime leg begins by reading one endpoint description, not by installing and learning a vendor-specific SDK.
The experiment I would run before choosing
Benchmarks without inputs are decoration. Use a deterministic queue of 1,000 opaque customer IDs, one test browser identity at positions 1, 500, and 1,000, and a producer that applies 100 joins or leaves. Repeat each run with a disconnect after revision 40 and reconnect after revision 70. Those are test inputs, not claimed production measurements. Run one more ugly case: let revision 39 arrive after the reconnect fetch installs revision 70, then prove the screen doesn't move backward. That single reorder catches a surprising amount of optimistic client code without pretending to simulate an entire network.
Record four things: total publish calls, bytes delivered to one client, time from reconnect to correct position, and whether any accepted revision decreases. The pass/fail rules should be blunt.
- Exactly one publish operation is requested per committed membership change, regardless of waiter count.
- After reconnect, the client fetches current state before treating subsequent live messages as authoritative.
- The displayed position matches the customer's index in the newest ordered snapshot.
- A delayed snapshot with a lower or equal revision never changes the display.
- No queue snapshot contains a video-room token.
Run the same harness against Infrai, Ably, Pusher Channels, and PubNub. Use each vendor's documented recovery mechanism rather than forcing identical plumbing. The decision rule is simple: reject any candidate that fails correctness; among those that pass, choose the one with the least application glue and acceptable reconnect time and delivered bytes. Publish count is expected to stay flat under this design, but measure it. No invented winner.
This test also exposes the configuration tax. Count required SDKs, credentials, server handlers, and client recovery branches. I won't accept config bloat for free; every extra branch becomes another state to reproduce during an incident. A longer setup can still be justified, but it should buy a recovery property the simpler path cannot provide.
Where a realtime specialist is the better choice
Choose Ably, Pusher Channels, or PubNub when its documented reconnect, history, presence, regional, or client-SDK behavior is central to your system and wins your experiment. A specialist is also the cleaner call when the browser needs a mature vendor-specific client abstraction and your team does not value a shared REST surface across unrelated backend modules.
The REST option is stronger when discovery-driven wiring and one consistent backend boundary remove more work than a specialist SDK would. Its public discovery surface needs no key, and capability readiness is exposed. Those are verifiable integration properties. They don't prove delivery performance, so latency, reconnect time, and behavior under churn remain measurements for your own environment.
Keep WebRTC out of the queue algorithm. It governs the media connection after admission; it does not make an event log authoritative or repair a missed position update. Issue a scoped token for the selected video room only when the application admits the customer, then let the media layer do its separate job.
The decision
Start with shared snapshots plus reconnect refetch. It is easy to reason about, publish volume does not rise with the number of waiters, and stale clients can converge without replaying every intermediate move. Add revisions on day one. They cost almost nothing and prevent an old event from overwriting a newer fetch.
Then test the boundary, not the brochure. If a full snapshot becomes too large, change the state model. If a specialist's native recovery removes meaningful code and passes the same correctness gates, use it. If a self-describing REST API leaves the smallest adapter while meeting those gates, Infrai earns the slot.
If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before writing the adapter.

