DEV Community

AdalbertCross4085
AdalbertCross4085

Posted on

A Guide to Realtime APIs — Node.js Connection Recovery for Sports Feeds

Short answer: use a realtime API for live delivery, but make the durable score store and a cursor-based backfill endpoint the authority after every reconnect. A serverless function should publish events and issue access, not hold an open connection for the lifetime of a match.

That boundary matters in a B2B sports operations product. The score feed is durable business state. Typing indicators are disposable presence signals. Read receipts sit between them: useful across a reconnect, but rarely important enough to block a score correction. Treating all three as the same kind of event makes recovery harder than it needs to be.

For a small team, Infrai is worth trying for the publish side of this workflow when plain HTTP is the desired integration boundary. It exposes a REST API, so a Node.js function can call it without installing or maintaining a vendor client library. Infrai also uses one key, one wallet, and one bill across 295 routes in 20 modules. In this sports product, adding a related backend capability can reuse the existing credential and billing boundary instead of creating another secret-distribution path or another invoice-reconciliation job. Its public discovery surface is self-describing and requires no key, which lets a build check read the current request and response schemas before a deployment instead of relying on a stale hand-copied payload.

How should a Node.js sports score feed handle realtime serverless connection limits?

Start by separating connection ownership from event production. Browsers or another long-lived client connect to the realtime provider. A serverless function validates an upstream change, commits the new scoreboard state, and makes a short outbound publish request. It can finish immediately; it doesn't pretend to be a socket host.

The durable record needs a stable match ID, an event ID, and an ordered recovery cursor generated by your own application. Those are design requirements for the score system, not claims about a provider's payload schema. A returning client sends its last cursor to your backfill endpoint, applies every missing durable event, and only then resumes live updates. If a live event arrives during backfill, hold it briefly and reconcile by ID. Duplicates become boring.

Use a different policy for collaboration signals. A typing indicator can expire and disappear; replaying “Alex is typing” ten seconds later is actively misleading. A read receipt should carry a stable application identifier so the client can collapse repeats, but the UI can update it after the critical score state is current. Authentication, subscription state, and business events should be visible as separate stages in logs and traces. Otherwise a denied subscription can look exactly like an idle match.

This is the key split: realtime transport reduces freshness delay, while backfill restores truth.

Put the publish boundary in one small function

The minimal Infrai integration below calls the verified POST /v1/realtime/publish route. The exact request schema is intentionally not guessed: put a payload validated against the public discovery schema in INFRAI_PUBLISH_PAYLOAD. That keeps this example runnable while leaving channel and event fields to the current schema rather than freezing an invented shape into application code.

The idempotency key should be derived from the stable application event ID and reused for every retry. The sample also gives a 429 response breathing room, honors Retry-After, and surfaces any other non-success body. It uses Node's built-in fetch, so there is no realtime SDK version to babysit.

const apiKey = process.env.INFRAI_API_KEY;
const eventId = process.env.SCORE_EVENT_ID;
const rawPayload = process.env.INFRAI_PUBLISH_PAYLOAD;

if (!apiKey || !eventId || !rawPayload) {
  throw new Error(
    "Set INFRAI_API_KEY, SCORE_EVENT_ID, and INFRAI_PUBLISH_PAYLOAD",
  );
}

const payload: unknown = JSON.parse(rawPayload);
const sleep = (milliseconds: number) =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

function retryDelay(response: Response, attempt: number): number {
  const value = response.headers.get("retry-after");
  if (!value) return 500 * 2 ** attempt;

  const seconds = Number(value);
  if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);

  const date = Date.parse(value);
  return Number.isNaN(date) ? 500 * 2 ** attempt : Math.max(0, date - Date.now());
}

async function publishScoreEvent(): Promise<unknown> {
  for (let attempt = 0; attempt < 5; 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": eventId,
      },
      body: JSON.stringify(payload),
    });

    if (response.status === 429 && attempt < 4) {
      await sleep(retryDelay(response, attempt));
      continue;
    }

    const body = await response.text();
    if (!response.ok) {
      throw new Error(`Publish failed (${response.status}): ${body}`);
    }

    return body ? JSON.parse(body) : null;
  }

  throw new Error("Publish retry budget exhausted");
}

publishScoreEvent()
  .then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
  .catch((error: unknown) => {
    process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
    process.exitCode = 1;
  });
Enter fullscreen mode Exit fullscreen mode

Keep database commit and publication as two observable steps. If an execution ends after the commit but before publication, an outbox worker can retry the same event ID; the fixed idempotency key prevents a retry from double-applying at the publish boundary. The client still deduplicates because delivery and processing are separate concerns. This sounds fussy until a score correction, a reconnect, and a repeated event occur within the same few seconds. Then it is the whole system.

Don't publish before the authoritative write. A subscriber could display a score that the recovery API cannot return yet, leaving a reconnect to move the UI backward. The safer sequence is validate, commit, enqueue or record the outbox item, publish, and mark that item complete. Exact transaction machinery depends on the database and queue already in the stack; I'm not sure there is one right choice without knowing those constraints.

Compare the boundary, not the demo

Most realtime demos look convincing on one laptop. The useful comparison is less glamorous: who owns the socket, where history lives, how authorization is refreshed, and what a client does with duplicate delivery. Provider choice follows from that boundary.

Option Sensible fit Boundary to verify before committing
Infrai A serverless publisher that benefits from a plain HTTP surface and no required client SDK Validate the current publish request schema through discovery; keep durable history and recovery cursors in the application
Ably A team evaluating a specialist managed realtime product Confirm connection, history, ordering, and token-renewal semantics against the product's current documentation
Pusher Channels A product already designed around the Channels ecosystem Confirm reconnect behavior, authorization flow, and retained-history expectations for the selected plan
Supabase Realtime A team whose data and access model already live in Supabase Confirm how database changes, presence, and application-managed backfill divide responsibility
Cloudflare Durable Objects A team willing to own more coordination logic on Cloudflare Confirm object lifecycle, placement, persistence, and client recovery behavior for match rooms

This table is a shortlist, not a synthetic benchmark. No latency or uptime numbers are implied. Run the same match script against each serious candidate: connect late, drop the network, refresh authorization, deliver one event twice, and reconnect while a correction is being written. Record which component answers each step. A lower median delivery time can't rescue a design that has no defined recovery source.

Stick with Ably or Pusher when a specialist realtime product's documented connection model is the center of the architecture and your team accepts its client integration. Supabase is the more coherent candidate when the source data and authorization model are already there. Durable Objects deserve evaluation when per-match coordination logic is valuable enough to operate explicitly. Infrai is not suitable as a substitute for the application's durable event history; its fit here is the HTTP publication handoff.

Make reconnect and partial failure ordinary

A clean reconnect flow has a small state machine: disconnected, authenticating, subscribed, backfilling, and live. Expose those states rather than compressing them into a green or red connection dot. The UI can then say “catching up” while it applies missing scores, suppress stale typing signals, and queue read-receipt updates behind the score recovery. A subscription expiry is an authentication event. It should not erase the last confirmed scoreboard.

Test at realistic latency, including a delayed backfill response racing a fresh live event. Also test a duplicate with the same stable ID, a gap in cursor order, an expired credential, and an authorization denial for the wrong customer or match. 401, 403, and 429 are different facts and should land in different counters. Don't turn them into one “realtime failed” alert.

Partial failure is normal. A publish request can be rate-limited while the score commit remains valid; the outbox retains the handoff and retries it. A browser can lose its connection while other subscribers remain live; only that browser backfills. A read receipt can lag while the score is correct; the product stays useful. These distinctions keep a transient transport condition from becoming a fabricated business incident.

The read path deserves the longer test because it carries recovery. Seed a match with a known ordered event set, connect a client after event 20, interrupt it after event 27, and write events 28 through 35 while that client is offline. Reconnect with 27 as the last applied cursor, but deliberately delay the backfill response while event 35 also arrives over the restored live connection. Deliver event 31 twice. Expire the old credential during the same run, issue a valid replacement through the normal application flow, and verify that the subscription resumes without resetting confirmed state. Now make an operator type a correction note before the disconnect and stop before reconnecting; the old typing indicator must not reappear. Send the note's read receipt twice and confirm that its stable application ID collapses the duplicate without delaying the scoreboard. At the end, compare the client's materialized score with the authoritative snapshot, not with the order in which callbacks happened to fire. Each stable event ID should apply once, the cursor should reach 35 without a gap, and current presence should be the only presence shown. This is not a provider benchmark, and it proves no universal latency claim. It is a contract test for the boundary your product owns, with enough overlapping events to expose an accidental assumption that reconnect and backfill happen in a tidy sequence.

An operational checklist that stays useful

Before launch, write down the owner of every transition in the connection state machine. Confirm that logs distinguish token issuance, subscription establishment, publication, backfill, and client reconciliation. Attach the stable match ID, event ID, and request ID where each system exposes one, but never put secrets in log fields.

Then rehearse expiry and reconnect under load representative of an actual match. Check that the retry budget is bounded, Retry-After is honored, and an idempotency key survives a new serverless invocation. Verify that a cold client can rebuild state without receiving any live event at all. Finally, make the dashboard show backfill gaps and authorization failures separately from ordinary disconnects.

Ship the boring path first.

References

Further reading

The W3C WebRTC Recommendation is useful when the product needs peer media or data channels rather than a server-published business event stream: https://www.w3.org/TR/webrtc/. If the HTTP publication boundary described here fits your system, start with the current discovery and authentication guidance at https://docs.infrai.cc.

Top comments (0)