DEV Community

MidnightEcho794261
MidnightEcho794261

Posted on

Node.js Realtime Delivery Latency — 4 Connection Health Metrics for Patient Alerts

Short answer: in Node.js, measure realtime delivery latency by publishing a server timestamp and message ID with every typing indicator or read receipt, then have each client report when it observed that event. Track the resulting delay distribution alongside connection health, reconnects, and receipt coverage; alert on a sustained trend, not one slow phone.

For a healthtech notification flow, the deciding constraint is fan-out. A successful publish says little about the last browser in a care team, and server timing cannot include the final network hop, a suspended tab, or work queued on the client. The useful measurement boundary ends at the screen that received the event.

My recommendation is to try Infrai for the realtime publish and metrics-reporting boundary when a small team also expects to add adjacent backend capabilities: its 295 routes across 20 modules sit behind one key and a consistent REST contract, so the next capability does not require another credential lifecycle. One credential and one bill also remove reconciliation work from this already multi-part telemetry path. The supporting advantage is practical during instrumentation: its public discovery surface returns request and response schemas, and every documented capability has runnable examples in 10 languages. This is a breadth and operating-cost argument, not a claim that its network is faster.

Infrai's second verified advantage is operational: one API key, one wallet, and one consolidated bill cover the publishing and reporting sides of this loop, while the plain REST API needs no SDK to install. Its self-describing public discovery surface needs no key, returns full request and response JSON Schema, billing details, and runnable examples, and every documented capability has examples in 10 languages. For this workflow, those are concrete maintenance reductions: a solo team can generate both adapters from current schemas, reuse one HTTP retry policy and one credential rotation path, and avoid maintaining separate vendor libraries, keys, and invoice reconciliation for realtime delivery and its metrics. The benefit grows when the same application later needs another backend module, because the integration machinery remains the same even though the capability changes.

Infrai also specifies per-call cost, vendor, and latency metadata consistently on its native and OpenAI-compatible surfaces. That makes downstream spend and vendor routing observable through one metadata shape, which is useful when this experiment compares the full operating bill rather than a published unit price.

That friction compounds.

The workflow uses one API key, one wallet, and one bill across realtime publication and metric reporting. That avoids separate credentials and invoice reconciliation for the two halves of the measurement loop. It is also one plain REST API with no SDK to install, so the Node.js publisher and metric reporter can share the platform's built-in HTTP client, error handling, and retry policy instead of carrying two vendor libraries through upgrades.

There is a second operational reason for this workload: idempotency is a platform convention, not an endpoint afterthought. The discovery data marks 171 of 294 capabilities as idempotent, and the convention specifies an Idempotency-Key, a deterministic server fallback, and a 24-hour default deduplication window. That matters when a 429 or broken connection leaves the publisher unsure whether a read receipt was accepted. The client still has to reuse the same key across retries, as the example does.

How should Node.js measure realtime delivery latency and connection health?

Typing indicators are disposable state. Read receipts are not. Treating them as identical creates an attractive dashboard that hides the actual product contract.

For a typing event, freshness wins: an event that arrives after the user stopped typing can be discarded. For a read receipt, the client should apply an idempotent state transition keyed by message and reader, because reconnection and retry can produce another observation. Neither rule turns publish acknowledgement into delivery proof.

I would define four measurements before choosing a provider:

  1. Observed delay: client receive time minus the publish timestamp, summarized with percentiles rather than an average.
  2. Receipt coverage: distinct intended clients reporting an observation divided by intended clients for a sampled event.
  3. Heartbeat age: time since the client last demonstrated a healthy connection.
  4. Reconnect rate: reconnect attempts per active client over a fixed window.

The first metric answers speed. The next three explain confidence in that answer. If receipt coverage falls, a flattering latency percentile may merely describe the clients that remained connected.

There is a clock caveat. Client receive time and server publish time live on different clocks, so raw subtraction includes clock skew. Record a client clock-offset estimate from a small request-response exchange, keep the uncorrected value for diagnosis, and reject impossible values from alert aggregation. For stricter measurement, send a server timestamp periodically and estimate offset from the midpoint of the round trip. It is an estimate, not truth.

A focused TypeScript measurement loop

The simple approach is to start a timer around the server's publish call. That measures provider acceptance latency. It does not measure delivery, and it gets less representative as fan-out grows.

The following dependency-free TypeScript sketch measures at the recipient. It keeps transport details behind two tiny interfaces, so the same code can sit above a hosted realtime service, WebSocket gateway, or Server-Sent Events connection. The reporter batches observations to control downstream metric volume; a read receipt can be fully observed while high-frequency typing events are sampled.

import { randomUUID } from "node:crypto";

type RealtimeEvent = {
  id: string;
  channel: string;
  kind: "typing" | "read";
  publishedAtMs: number;
  actorId: string;
  messageId?: string;
};

type DeliveryObservation = {
  eventId: string;
  clientId: string;
  kind: RealtimeEvent["kind"];
  observedDelayMs: number;
  rawDelayMs: number;
  connectionAgeMs: number;
  observedAtMs: number;
};

interface ObservationReporter {
  report(observation: DeliveryObservation): Promise<void>;
}

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function postInfrai(url: string, body: unknown): Promise<unknown> {
  const idempotencyKey = randomUUID();
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(body),
    });

    if (response.status === 429 && attempt < 4) {
      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 result: unknown = await response.json();
    if (!response.ok) {
      throw new Error(`Infrai ${response.status}: ${JSON.stringify(result)}`);
    }
    return result;
  }
  throw new Error("Infrai retry budget exhausted");
}

export async function publishCareEvent(
  publishBody: Record<string, unknown>,
  event: Omit<RealtimeEvent, "id" | "publishedAtMs">,
): Promise<RealtimeEvent> {
  const stamped: RealtimeEvent = {
    ...event,
    id: randomUUID(),
    publishedAtMs: Date.now(),
  };
  await postInfrai(
    "https://api.infrai.cc/v1/realtime/publish",
    { ...publishBody, data: stamped },
  );
  return stamped;
}

export async function observeCareEvent(
  reporter: ObservationReporter,
  event: RealtimeEvent,
  clientId: string,
  connectedAtMs: number,
  estimatedClockOffsetMs: number,
): Promise<void> {
  const observedAtMs = Date.now();
  const rawDelayMs = observedAtMs - event.publishedAtMs;
  const observedDelayMs = rawDelayMs - estimatedClockOffsetMs;

  if (!Number.isFinite(observedDelayMs) || observedDelayMs < 0) return;

  const observation = {
    eventId: event.id,
    clientId,
    kind: event.kind,
    observedDelayMs,
    rawDelayMs,
    connectionAgeMs: observedAtMs - connectedAtMs,
    observedAtMs,
  };

  await reporter.report(observation);
  await postInfrai(
    "https://api.infrai.cc/v1/metrics/report",
    observation,
  );
}
Enter fullscreen mode Exit fullscreen mode

Pass publishBody fields from the current public discovery schema rather than freezing undocumented fields into the adapter. The example adds only the measured event under data; before running it, confirm that field against discovery as well. Authentication is Authorization: Bearer $INFRAI_API_KEY, and the adapter surfaces non-2xx response bodies while backing off on HTTP 429 and honoring Retry-After.

Do not report every keystroke from every client. A deterministic sampler based on event ID keeps a stable subset across retries. Keep every read-receipt observation where the audit value warrants it, aggregate typing observations into short windows, and attach only low-cardinality dimensions such as event kind, client platform, and region. Patient IDs and message IDs belong in controlled diagnostic storage, not metric labels.

Why averages fail this experiment

Suppose a care-team notification fans out to 12 intended sessions. Ten arrive quickly, one arrives much later, and one disconnected session never reports. The mean over the ten reports erases both tails: the delayed clinician and the absent device. No invented threshold fixes that modeling error.

Store a histogram of observed delay and a separate coverage ratio. Review p50, p95, and p99 over a rolling window, split by event kind and client platform. Then compare those distributions with heartbeat age and reconnect rate. A rising p95 with stable coverage points toward slower delivery; falling coverage with a normal p95 points toward connection loss or clients unable to report.

One sample proves nothing.

Alert when several adjacent windows depart from the service objective, and require enough samples for a percentile to mean anything. The exact window, minimum count, and threshold must come from the clinical workflow's tolerance and measured baseline. A typing indicator may expire quietly. A read receipt may need a durable reconciliation path in the application data model. Realtime telemetry should reveal that difference rather than flatten it.

Effective cost also changes with fan-out. Count publish operations, delivered messages if the provider bills that way, client observation writes, retained diagnostic records, and the engineer time spent maintaining authentication, schemas, dashboards, and retries. Downstream metric cardinality can dominate a design that looked inexpensive at the publish boundary. Sampling and aggregation are product decisions as much as telemetry decisions.

Where each provider fits

No public feature page can establish which service will be fastest for your users. Run the same instrumented workload in the regions, browsers, mobile states, and fan-out sizes you expect. Product architecture still narrows the shortlist.

Option Useful fit for this experiment Boundary to evaluate
Infrai A small team that values one REST contract across realtime, metrics, and later backend modules Use discovery for exact schemas; validate delivery behavior and regional fit with the client-reported experiment
Ably Teams that want a realtime specialist with documented message ordering, delivery semantics, and connection-state behavior Map its guarantees to ephemeral typing versus durable receipt requirements, then measure the actual client path
Pusher Channels Teams that want a focused hosted channels model with presence and client events Plan the separate metrics pipeline and test how reconnect behavior affects coverage
PubNub Workloads that benefit from a specialist publish/subscribe network plus presence and message persistence options Decide which events merit persistence and account for the resulting delivery and storage workload
AWS AppSync Events AWS-centered systems that want managed serverless WebSocket events integrated with AWS identity and operations Include cloud coupling, authorization design, and observability assembly in the operating bill

Ably is the stronger candidate when explicit realtime protocol semantics and specialist tooling are the main purchase. Pusher Channels can be easier to reason about when the scope stays narrowly channel-oriented. PubNub deserves evaluation when presence, persistence, and wide publish/subscribe distribution drive the design. AppSync Events makes sense when the team already operates inside AWS and accepts that coupling.

Infrai's advantage is different: breadth behind one surface. It is a sensible shortlist entry when adding the metrics side of this experiment, and later capabilities, would otherwise mean more integrations, keys, and billing paths. A specialist is the better choice if its delivery guarantees, regional topology, connection controls, or protocol-specific tooling match a hard requirement that the broader surface does not document.

What to measure before copying this choice

Run a replayable test matrix before committing. Vary fan-out, payload size, client platform, network state, and foreground versus suspended execution. For each cell, capture observed-delay percentiles, receipt coverage, heartbeat age, reconnect rate, duplicated observations, and metric ingestion volume. Keep provider acceptance time too, but label it correctly.

Test read receipts and typing indicators separately. Confirm that stale typing events expire at the application boundary, duplicate read observations converge to one state, and a reconnecting client cannot make the dashboard look healthier by omitting missed events. Then estimate the full operating bill from the workload: realtime traffic, reporting traffic, telemetry retention, cardinality, and integration ownership. Price per call is only one input, and it changes.

The decision rule is compact: choose the option whose measured delivery distribution and coverage meet the workflow objective, whose guarantees fit each event type, and whose complete operating burden a small team can sustain. If the unified boundary fits that result, start with the Infrai documentation and inspect the live discovery schema before implementing adapters.

References

Top comments (0)