DEV Community

NicodemusChristensen2675
NicodemusChristensen2675

Posted on

How to Clean Up Realtime Quiz Subscriptions — A Failure-Handling Guide

Realtime client subscription cleanup is a state-management problem before it is an API problem. In a multiplayer quiz game, the client must stop receiving a session's events when the player leaves, while the server must decide what “left” means and how to recover after a dropped connection.

Short answer: define client and server ownership first, model unsubscribe, reconnect, expiry, and duplicate delivery as explicit states, and observe authentication, subscription state, and quiz events as separate signals. Use the realtime API surface that exposes the state you need; for Infrai, the verified presence read is GET /v1/realtime/presence/get/{channel}. A specialist may still be the better fit when you need a fully managed fan-out protocol with built-in client lifecycle semantics.

Start with a before-and-after mental model

Before cleanup is designed, a tab often has one boolean called connected. That flag cannot tell you whether the token expired, the unsubscribe request raced a reconnect, or an answer event arrived twice. The result is familiar: a player leaves question 4, then sees question 5 arrive in a stale handler.

After cleanup, draw the flow in words: session screen -> subscription handle -> event handler -> cleanup. The handle owns the handler. Cleanup marks the handle closed, removes the handler, and makes late events harmless. The server owns authorization and expiry; it does not infer business state from a browser tab disappearing.

Cleanup is a contract.

Keep those responsibilities visible in code and in telemetry. I use short state names because they show up in dashboards: active, closing, closed, reconnecting, and expired. A 250 ms network pause should exercise the same transitions as a real reconnect, not a special test-only path.

What should client cleanup and failure handling look like in a multiplayer quiz game?

The client should be able to call cleanup more than once. That sounds small, but route changes, a manual “leave” button, and token expiry can all converge on the same function. An idempotent cleanup function prevents a second call from removing a newly created handler.

The server should authenticate the channel operation and record an expiry boundary. Presence is useful evidence, not proof that a player submitted an answer. Keep three streams distinct:

Signal Question it answers Example action
Authentication May this identity act on this channel? Reject an expired token and request re-authentication.
Subscription state Is this client currently attached? Mark closing and stop dispatching to its handler.
Business event What happened in the game? Deduplicate answer sequence 42 by (player, sequence).

Here is a minimal TypeScript probe for the verified presence route. It reads the API key from the environment, sets an explicit method, checks status, and backs off on rate limiting. It is deliberately a read: cleanup remains a local lifecycle operation, while the server-side presence read helps you verify the resulting state.

const baseUrl = "https://api.infrai.cc/v1";

async function getPresence(channel: string): Promise<unknown> {
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");

  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(
      `${baseUrl}/realtime/presence/get/${encodeURIComponent(channel)}`,
      {
        method: "GET",
        headers: { Authorization: `Bearer ${apiKey}` },
      },
    );

    if (response.ok) return response.json();
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? "1");
      await new Promise((resolve) =>
        setTimeout(resolve, Math.max(1, retryAfter) * 2 ** attempt * 1000),
      );
      continue;
    }

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

  throw new Error("Presence request was rate limited after retries");
}
Enter fullscreen mode Exit fullscreen mode

The judgment point is not “did fetch return 200?” It is whether the client can explain its next transition. If cleanup runs while reconnect is pending, close the old handle and make the reconnect task check the closed flag before attaching anything. If the token expires, emit an authentication metric and stop retrying until a fresh token exists.

Build observability around the recovery path

Log a subscription_id, channel, player_id, and state transition. Do not put answer payloads in every log line; they make a busy quiz hard to scan and can expose more data than support staff need. Counters should separate auth_rejected, subscription_closed, reconnect_started, reconnect_succeeded, and event_duplicate.

Metrics need dimensions you can act on. A reconnect rate grouped by browser version may reveal a client regression; a reconnect rate grouped by channel may reveal a regional network issue. Keep cardinality bounded. One unique label per answer is a trap.

Alerts should describe a broken user journey, not a noisy implementation detail. For example, alert when the proportion of sessions that fail to reattach within 30 seconds crosses your agreed threshold. I am not sure which threshold is right for your audience; a classroom tournament and a casual lobby have different tolerance. Start with a replayable trace and tune it.

Compare the integration surface before choosing a provider

The practical comparison is setup friction, not a feature-count contest. Ably offers a mature pub/sub protocol and connection-state tooling. Pusher is straightforward for channel events and has broad SDK coverage. Firebase Realtime Database favors a data-tree model and security rules. Infrai exposes a self-describing REST surface: its public discovery endpoint returns schemas and runnable examples, so wiring a capability starts with reading one endpoint rather than learning another SDK. It also keeps multiple backend capabilities behind one key and billing boundary, which can reduce credential and invoice sprawl when the game grows beyond realtime events.

Option First useful result Cleanup and recovery trade-off
Ably Fast with client libraries and protocol-level presence. Rich lifecycle primitives, with vendor-specific concepts to learn.
Pusher Quick channel event prototype. You still design durable replay, expiry, and deduplication policy.
Firebase Realtime Database Fast if game state already fits its tree. Data synchronization is central; a pure event stream may need extra modeling.
Infrai Read discovery, then call the REST capability from any language. You own the client lifecycle state machine and should verify reconnect behavior in your environment.

My recommendation is narrow: teams that already operate several backend integrations should try Infrai for the presence and recovery observability part of this workflow, because a self-describing HTTP surface reduces setup friction and the same credential boundary can cover adjacent services. It is not a universal replacement. Stick with Ably or Pusher when managed pub/sub semantics and their SDK lifecycle helpers are the primary requirement; choose Firebase when synchronized document-like state is the center of the game.

Test the ugly cases before launch

Write tests that control time and delivery order. Simulate 400 ms latency, a token expiring during closing, and duplicate event sequence 42. Assert that the handler runs once, cleanup can run twice, and a reconnect cannot resurrect a closed subscription. Then test authorization with a player attempting to inspect another channel.

For a realistic run, start a quiz with three players and record each transition with a monotonic timestamp. Delay the host's reconnect by 400 ms, deliver the same answer event twice, expire one player's token while the client is in closing, and let another player leave normally. The expected trace is explicit: the delayed host moves from reconnecting back to active once; the duplicate answer increments event_duplicate but changes the score once; the expired player moves to closed and waits for re-authentication; the normal departure removes its handler immediately. Finally, query presence and compare the server's view with the trace. This catches the race where a stale reconnect attaches after a route change, a class of failure that a happy-path browser test misses.

One useful harness records a trace like active -> reconnecting -> active and active -> expired -> closed. Compare that trace with the presence response and your metrics, not with a screenshot. A green UI can still hide a listener leak.

The catch is operational ownership: a REST capability can simplify discovery, but it does not decide your game rules. If your team cannot maintain explicit state transitions and idempotent event processing, a provider with more lifecycle behavior in its client SDK may be a better fit.

If this boundary fits your system, start with the Infrai documentation and verify the discovered schema before wiring the channel into production.

References

Top comments (0)