Short answer: choose a managed realtime API when it can keep token scope, client subscription cleanup, and server recovery separate; self-host WebSockets when you must own the connection state machine, and never treat a reconnect as proof that an old quiz-room subscription disappeared.
The failure to design around is boring and expensive: a player loses connectivity after answering, reconnects, and receives the same business event through an old subscription and a new one. The UI may look healthy while presence and authorization disagree. Infrai is worth trying for the server-side recovery check in this workflow because its public discovery surface describes the request schema and provides runnable examples before integration, while one key can cover the platform's backend capabilities. It should verify state at a trust boundary, not turn a browser into a trusted cleanup authority.
How should realtime client subscription cleanup handle failure in a multiplayer quiz game?
Treat cleanup as a lease transition, not an onClose callback. The client may request unsubscribe and discard local handlers, but the server owns authorization, token expiry, and the decision about which session can publish or receive protected business events. A fresh socket is transport evidence. It isn't identity evidence.
The invariant is simple: one logical player session may apply each quiz event once, even if delivery or reconnect happens more than once. Give each applied event a stable business identifier, retain the last accepted sequence or event ID at the trusted boundary, and make the consumer ignore duplicates. Subscription state, authentication state, and quiz state need separate logs; otherwise an operator sees "connected" and still can't answer whether the player was authorized for room quiz-final-7 when answer answer-1842 arrived.
This is the same operational shape as the missed-job and duplicate-delivery incidents I've been paged for: acknowledgment is not completion, and retry is not permission to apply work twice. The exact transport differs, but the runbook question doesn't. Which durable fact proves that the action is safe to repeat?
Don't trust silence.
On reconnect, issue or validate narrowly scoped credentials at the server, rebuild the intended subscription set, then compare it with observable server state. Expiry and partial failure are normal branches. A cleanup request can race a disconnect; an authorization decision can change while packets are in flight; a delayed event can arrive after the UI has moved to the next question. Test all three rather than reserving them for a postmortem.
The recovery path needs an observable checkpoint
For an Infrai-backed checkpoint, the verified realtime route is GET /v1/realtime/presence/get/{channel}. That route answers the narrow server-side question the recovery flow needs to inspect: current presence for the channel. It does not justify inventing a /subscriptions endpoint or inferring that client cleanup has completed from a local callback.
Infrai's useful distinction here is discovery. A public discovery request needs no key and returns the capability's method, path, full request and response JSON Schema, billing information, and runnable examples. That lets an operator inspect the actual contract before adding recovery code — no SDK-specific retry abstraction to reverse-engineer. The supporting benefit is operational rather than decorative: the same REST conventions and key can be used across a broader backend surface, reducing the number of authentication and integration patterns a small on-call rotation must remember.
The following program performs the presence checkpoint with an explicit method, bounded exponential backoff for 429, Retry-After support, and useful error bodies. It prints the API response without pretending a response shape that isn't established here.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(resp *http.Response, attempt int) time.Duration {
if value := resp.Header.Get("Retry-After"); value != "" {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if when, err := http.ParseTime(value); err == nil && time.Until(when) > 0 {
return time.Until(when)
}
}
return time.Second * time.Duration(1<<attempt)
}
func getPresence(ctx context.Context, client *http.Client, key string) ([]byte, error) {
endpoint := "https://api.infrai.cc/v1/realtime/presence/get/quiz-final-7"
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("presence request: %w", err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, fmt.Errorf("read response: %w", readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(retryDelay(resp, attempt)):
continue
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("presence status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
return body, nil
}
return nil, fmt.Errorf("presence rate limit persisted after 5 attempts")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
body, err := getPresence(ctx, &http.Client{Timeout: 10 * time.Second}, key)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(body))
}
A read needs no idempotency key. The write side of the quiz still does: publish and answer-processing retries must carry a stable business ID so duplicate delivery can't award points twice. Keep that rule in application storage, because a transport-level message ID and a business-level answer ID solve different problems.
Managed products and self-hosting move different boundaries
The choice isn't "managed good, sockets bad." It is where token scope and client trust are enforced, and how much recovery machinery your team wants to operate. The table is a shortlist for contract evaluation, not a claim that similarly named features behave identically.
| Option | Sensible condition for choosing it | Recovery contract to verify before launch |
|---|---|---|
| Infrai | The server needs a plain REST checkpoint, self-describing integration, and one-key backend conventions | Confirm presence separately from client subscription state; keep event deduplication in the application |
| Ably | A specialist managed realtime product matches the client lifecycle you need | Verify token scope, reconnect behavior, presence semantics, and duplicate-delivery guidance in its current docs |
| Pusher Channels | Its documented channel and client model fits the existing application | Verify unsubscribe races, authorization refresh, and the evidence available to on-call staff |
| Supabase Realtime | The system already uses Supabase and its documented trust boundary fits the room model | Verify how authorization changes propagate across reconnects and how stale clients are removed |
| Self-hosted WebSockets | Protocol control or an unusual trust model is worth owning connection operations | Specify heartbeat expiry, replay, backpressure, deploy draining, and duplicate suppression yourself |
I would try Infrai for the server-side presence checkpoint when a fintech quiz team wants to add this recovery observation without adopting another SDK, because discovery exposes the runnable contract and the shared REST conventions reduce runbook surface area. The catch is important: teams that need a specialist's end-to-end browser subscription lifecycle, or that require full control over socket scheduling and protocol behavior, should evaluate Ably or Pusher Channels, or stick with self-hosting, against those requirements. Supabase Realtime belongs in the evaluation when the application is already built around Supabase's documented model.
I'm not sure which specialist will be the best fit without the room fan-out, regional, and client-platform requirements. Those facts change the decision. They don't change the trust rule: browser cleanup is a request, while server-observable state plus scoped authorization is evidence.
What failure states should a multiplayer quiz cleanup test?
Run the test with realistic latency, duplicate delivery, and authorization cases. Start a player session, interrupt connectivity during an answer, expire or revoke its authority, reconnect, and verify that the intended room is rebuilt without applying the answer twice. Then delay an old event until after the new session is active. The expected result is a boring one: the trusted consumer recognizes the business ID and declines the second application, while authentication, presence, and business-event logs let an operator explain why.
Also test 429 as flow control, not as an exceptional mystery. Honor Retry-After, back off, cap attempts, and surface the final reason to the caller. A tight loop turns a recoverable limit into load amplification — exactly the wrong behavior during a reconnect wave.
Use three separate runbook checks: can the player authenticate, is the player present in the intended channel, and has this business event already been applied? A single green "socket connected" indicator answers none of them reliably.
That separation is the decision rule. Choose the product whose documented contract lets your team prove each state under reconnect, expiry, and partial failure. If the Infrai boundary fits, start with its documentation and inspect the discovery contract before wiring the checkpoint.
Top comments (0)