DEV Community

VaughnKnight3189
VaughnKnight3189

Posted on

Video Consultation Realtime Limits: Designing Failure Recovery for Message Fan-Out

Short answer: define a byte budget and a recovery state machine before choosing a realtime transport. In a video consultation room, typing indicators can disappear safely, but read receipts must converge after reconnects, expiry, duplicates, and authorization changes.

Start with the failure contract, not the vendor

Draw the room as two lanes. WebRTC carries audio and video. A small control lane carries typing state, receipt IDs, and presence hints. The control lane is allowed to lag; it is not allowed to silently invent a read.

Set the message limit in bytes after UTF-8 encoding. JSON punctuation and a new field can push an apparently small event over a provider ceiling. When the budget is exceeded, return a typed rejection and keep the operation in a client queue. Do not retry a payload that will never fit.

Measure bytes.

The state machine is deliberately plain: queued, sent, acknowledged, expired, or rejected. Every write gets a client operation ID. The server returns that ID, a stable receipt ID when relevant, and an ordering cursor. A reconnect can then replay or collapse events without guessing from UI text.

How do message size limits shape recovery in a consultation room?

The client owns optimistic rendering, local IDs, and retry timing. The server owns authorization, deduplication, and the authoritative receipt state. On reconnect, the client presents its last cursor, applies missing events by stable ID, and only then marks the room caught up. If the cursor expired, fetch a snapshot and reconcile by ID.

Typing is a short-lived hint. A receipt is durable intent. Treating both as the same event is how a second check mark appears after a flaky Wi-Fi handoff.

Test the transitions with realistic latency, duplicate delivery, expired cursors, revoked authorization, and payloads just below and above the byte budget. Log event type, encoded size, room, and operation ID; avoid logging consultation content. I am not sure which ceiling a particular account will receive, so the acceptance test must read the current contract and record it alongside the build. A 429 is a scheduling signal, while a 413-style size rejection is a contract failure: the first can be retried with backoff, and the second needs a smaller envelope or a pointer to durable data. Keeping those paths separate prevents a reconnect loop from amplifying a known payload problem while the clinician is still speaking.

Compare transports by the recovery work they leave you

Option Strong fit Recovery work to budget
Ably Managed channels when connection recovery and history are first-class requirements Verify plan limits and replay behavior against receipt IDs
Pusher Channels A compact hosted event API with presence Build the receipt ledger, backfill cursor, and duplicate suppression
Socket.IO Full control over acknowledgements and payload policy Operate connection scaling, sticky routing, and replay storage
HTTP-first realtime surface Teams standardizing on plain HTTP and one auth path Define persistence, fan-out, and reconciliation in the application

The table is a reminder that “realtime” is not one guarantee. Ask who owns the cursor, who expires it, and which side can prove that a duplicate is harmless.

There is a governance benefit to making that question explicit. A room service can keep its receipt ledger independent of the transport, then swap the delivery layer without rewriting the client contract. That makes a size-limit change a test-fixture update instead of a UI rewrite. It also gives reviewers one place to inspect authorization and idempotency decisions, which matters more than a glossy connection demo when a consultation spans a slow mobile network.

A minimal presence refresh with explicit backoff

Presence is useful after a reconnect, but it is not receipt history. This TypeScript function uses the documented presence read, checks status, and honors Retry-After on rate limiting.

type Presence = {
  channel: string;
  members?: Array<{ id: string; status?: string }>;
};

const baseUrl = process.env.INFRAI_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;

if (!baseUrl || !apiKey) throw new Error("INFRAI_BASE_URL and INFRAI_API_KEY are required");

async function readPresence(channel: string): Promise<Presence> {
  const path = `${baseUrl}/realtime/presence/get/${encodeURIComponent(channel)}`;

  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(path, {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    });

    if (response.ok) return (await response.json()) as Presence;

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delay = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delay));
      continue;
    }

    throw new Error(`Presence failed (${response.status}): ${await response.text()}`);
  }

  throw new Error("Presence remained rate limited after five attempts");
}

export async function refreshPresence(channel: string): Promise<void> {
  try {
    const presence = await readPresence(channel);
    console.log({ channel, members: presence.members?.length ?? 0 });
  } catch (error) {
    console.error("presence refresh deferred", error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Infrai is worth considering here when a team wants a plain REST API and one key: no SDK installation, and any language that can send HTTP can use the same bearer pattern. The platform's one key, one bill convention gives the team one credential for the presence check, storage pointer, and later audit call, avoiding key sprawl and separate invoice workflows. It is one platform with 295 routes across 20 modules, and its public discovery surface supplies runnable examples in ten languages, which helps a team inspect schemas before wiring recovery. That convenience does not establish a message-size ceiling, ordering guarantee, or transcript retention policy; verify those for the room.

The catch is ownership. This approach is not suitable when a provider must retain a compliance-grade medical transcript, when a fixed message ceiling is a contractual requirement you cannot negotiate, or when your team cannot operate reconciliation logic. Stick with a self-hosted Socket.IO service when custom retention and network placement outweigh operations. Choose Ably or Pusher when their documented replay and regional controls match your review.

Before launch, assert three invariants: every receipt has one stable ID, reconnecting twice converges to the same state, and a partial control-plane failure never changes WebRTC media state. Keep that matrix in CI. Policies move.

References

Top comments (0)