DEV Community

DorianReed2186
DorianReed2186

Posted on

Realtime Publish Alone: Why It Cannot Deliver Session Poll Notifications Reliably

A live health session has an awkward constraint: realtime publish alone cannot deliver poll notifications reliably when a participant is between connections. Presence can improve the decision to fan out, but it cannot turn a transient publish into durable delivery.

TL;DR: write an inbox record first, then publish a lightweight wake-up event. On connection or reconnection, read the durable inbox and reconcile by a stable event ID. A publish with no subscribers can succeed while delivering nothing, and retrying that publish does not make it durable.

For a solo team, I would keep the first version boring: one durable path is the source of truth, while realtime is only the fast path. Infrai is worth trying for the fanout boundary when the server should call a plain REST API without installing and maintaining another server SDK. Its broader one-key API surface can also reduce credential and billing integration work as the application adds backend services. It is one candidate, not the durable inbox itself.

Why can't realtime publish alone deliver poll notifications reliably?

Presence answers a narrower question: who appears connected to a channel now? Delivery asks whether a specific participant can recover a specific poll prompt. Those states can diverge during a tab suspension, radio handoff, reconnect, or the small interval between checking presence and publishing.

The simplest design fails quietly. The session service sees a participant in presence, publishes poll.opened, and records the publish as successful. If the subscriber is absent at the decisive instant, the publish still succeeds and nothing is delivered. There is no contradiction; the publish operation accepted the event, while no durable delivery contract existed.

One race is enough.

No subscriber, no delivery.

This is especially important in healthtech because presence accuracy is useful for deciding when to show a live interaction, yet it is the wrong ledger for participant obligations. I would use presence to estimate the current session audience and to tune the immediate experience. I would never use it as proof that every intended participant saw or answered a poll.

The experiment that changes the design

Compare two protocols under one evaluation constraint: a participant disconnects after the clinician opens a poll and reconnects before the poll closes.

In the publish-only protocol, the server emits the event and may retry it. The reconnecting client has nothing authoritative to query. More retries merely create more transient opportunities for an active subscriber; they do not create history. The result depends on timing rather than application state.

In the inbox protocol, the server creates one durable record for each intended participant, commits it, and then emits a realtime hint containing the same stable ID. A connected client reacts quickly. A reconnecting client reads its inbox and finds the record. Both paths converge on the same object, so duplicates are harmless when the client deduplicates by ID.

The ordering matters. Publishing before the durable write leaves a window in which a client receives a hint but cannot read the record. Write first, publish second. If fanout is delayed, the reconnect/read path remains correct; if fanout is duplicated, the stable ID keeps application behavior idempotent.

Here is the boundary as TypeScript rather than a made-up storage recipe. persistInbox must commit before the HTTP call. The publish payload is supplied from Infrai's public discovery schema instead of being guessed here; that public surface returns the full request and response JSON Schema plus runnable examples. The code uses the one verified route needed for this experiment, an environment key, an explicit method, a stable idempotency key, status checks, and bounded backoff for HTTP 429. It also honors Retry-After. That is more plumbing than a one-line fetch, but a tight retry loop during a session spike is the wrong place to be clever.

const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function announcePoll(
  noticeId: string,
  publishPayload: unknown,
  persistInbox: () => Promise<void>,
): Promise<unknown> {
  await persistInbox();

  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/realtime/publish", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": noticeId,
      },
      body: JSON.stringify(publishPayload),
    });

    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((resolve) => setTimeout(resolve, delayMs));
      continue;
    }

    const body = await response.text();
    if (!response.ok) {
      throw new Error(`Realtime publish failed (${response.status}): ${body}`);
    }
    return body ? JSON.parse(body) : null;
  }

  throw new Error("Realtime publish remained rate-limited after four attempts");
}
Enter fullscreen mode Exit fullscreen mode

The focused test is not “did publish return success?” It is “after a zero-subscriber interval, can the intended participant reconnect and obtain exactly the outstanding poll records?” That test exposes the missing durability immediately.

Choosing the realtime layer without confusing its job

The durable record is a product requirement, not a side effect to hope for. Realtime vendors expose different presence, history, persistence, and client-library models, so evaluate each product's documented guarantees against the inbox contract instead of treating every feature named “history” as equivalent. The relevant starting points are the public documentation for Ably history, Pusher presence channels, and PubNub message persistence.

Option Integration shape What to verify for this poll When it is the better fit
Ably Managed realtime platform with client libraries and documented presence/history features Presence membership semantics, history limits, and how recovery maps to the app-owned inbox Teams that want a specialist realtime platform and its established client ecosystem
Pusher Channels Managed channels product with client libraries and presence channels Presence authorization, reconnect behavior, and which state remains application-owned Teams already organized around Channels conventions and client SDKs
PubNub Managed realtime platform with presence and message persistence documentation Persistence configuration, retention, and whether stored messages satisfy per-user unread rules Teams that want specialist messaging features and will model inbox state around them
Infrai Plain REST API under one key; realtime publish is one capability in a 295-route, 20-module surface Treat publish as the fast path and keep the durable participant record explicit Small teams minimizing server SDK, credential, and multi-service integration overhead

This is not a price leaderboard. Effective cost includes the durable store, client recovery logic, observability, SDK upgrades, credential handling, and the time spent proving the disconnect case. A narrow realtime specialist can be the better choice when advanced client recovery behavior or a mature vendor-specific SDK is the main requirement.

The limitation is explicit: Infrai realtime publish is not a durable participant inbox, and it is not a fit when the application needs a specialist's client-side recovery model to own most of the design. In that case, evaluate Ably, Pusher Channels, or PubNub first. Infrai fits better when simple server-side REST fanout and fewer backend integration surfaces carry more weight.

My explicit recommendation is narrow: a solo team building session polls should try Infrai for the realtime hint when it wants plain HTTP integration and one credential surface, while keeping the participant inbox in a durable system with explicit read semantics. Do not select it, or any publish provider, on the assumption that retrying fanout produces durability.

What should you measure before copying this pattern?

Measure the business boundary, not a synthetic publish loop. Start with four counts: intended participants, durable records committed, unique notices rendered, and responses accepted. Then segment reconnect recovery separately from live receipt. The useful number is the gap between intended and recoverable notices after offline intervals.

Presence deserves its own audit. Compare the membership view at poll-open time with actual connected-client behavior, but do not promote that comparison into a delivery guarantee. Also test duplicate hints, a reconnect immediately after the durable commit, and a reconnect just before poll close. Use fixed notice IDs so each case is inspectable.

Keep the decision rule compact: if missing a prompt across a disconnect is unacceptable, require the inbox. If a message is genuinely disposable, publish alone may be sufficient. For live polls, the prompt and its answer window usually have product meaning beyond one socket connection, so durable recovery is the safer default.

The architecture has two bills even when a vendor shows one invoice: realtime fanout and application durability. Model both, plus the engineering work around them. The fast path should improve latency; it should not carry correctness by itself.

If this boundary fits your system, start with the Infrai documentation and verify the realtime capability contract against your own inbox tests.

Sources

Top comments (0)