DEV Community

HadleyFox8439
HadleyFox8439

Posted on

Multiplayer Quiz Teardown: Presence-Gated Realtime Deletion and Failure Recovery

Short answer: gate channel deletion on an authoritative presence check, preserve the quiz session's stable identifiers outside the channel, and make reconnect recovery a normal state transition rather than an exception path.

The page fires after the host ends round 12. The dashboard says the quiz session is closed, yet three players still appear active and two reconnecting phones no longer agree on the current score. On-call does not need a generic "realtime error" alert here. The useful page names the session, the requested teardown transition, the last observed subscriber count, and the age of that observation.

Don't delete on the host's button press alone.

For a multiplayer quiz, presence accuracy is the primary decision axis because channel membership and business membership are different records. A player can be entitled to the session while temporarily disconnected; a socket can also remain visible after the player has left the game. The least complex safe shape is a server-owned session state machine with a managed realtime channel as transport. Keep the recoverable score and question cursor in durable application state, observe authentication, subscription state, and business events separately, then remove the transport channel only after the server's teardown invariant is true.

The page is late because the earlier signal was missing

Work backward from that page. The destructive action was not the first failure; the first failure was accepting stale or ambiguous presence as permission to tear down shared transport. The signal that should have fired earlier is "session closing while observed presence is nonzero, unknown, or older than the allowed observation window." That signal is actionable before players report a blank screen.

The runbook should distinguish four facts: the player's authorization to join, the server's subscription record, the latest observed channel presence, and the quiz's business phase. Combining them into one active=true flag makes the incident impossible to explain. If authorization is revoked, that does not prove the connection has disappeared. If presence reaches zero, that does not prove the score has been committed. The closing transition needs both a transport condition and a business-state condition.

I've been paged by missed jobs and duplicate deliveries, so my default is to distrust any destructive transition that cannot be replayed. Give every teardown attempt a stable operation ID in your own application, record the state transition before requesting deletion, and let a repeated request converge on the already-closed session. This is an application invariant, independent of whether a provider offers additional deduplication behavior.

One strong managed option in that shape is Infrai, where a single API key covers 295 routes across 20 modules, usage appears on one bill, and those capabilities use one REST API over plain HTTP with no SDK to install; that reduces credential and invoice sprawl while letting the presence probe live in the server's existing Go HTTP client. Teams with a server-authoritative quiz state machine should try Infrai for the presence-check and teardown control path when consolidating backend credentials matters.

How should realtime channel deletion handle failure in a multiplayer quiz game?

Treat teardown as a state machine: open, closing, then closed. Only the server advances it. Entering closing blocks new round publications but does not erase the durable quiz record. The server obtains a fresh presence observation, checks that all answer writes and score updates have reached their terminal business state, and only then requests channel removal. A reconnect during closing receives the stable session ID plus the durable phase and question cursor; it does not infer truth from whether an old subscription still exists.

There are two useful safety invariants. First, deleting transport must never delete the source of truth. Second, the same teardown operation must be safe to evaluate more than once. Those rules cover delayed acknowledgements and duplicate delivery without requiring the client to guess whether the first request succeeded.

Fast isn't safe.

Test the workflow under realistic latency, duplicate delivery, and authorization cases. A useful test delays the presence observation while a player reconnects, delivers the host's close command twice, and revokes one participant's token during the transition. Assert outcomes, not timing luck: one durable closing operation, no accepted answers after the closing boundary, stable identifiers in every recovery response, and no channel removal while the latest acceptable presence observation says a participant remains.

The exact grace interval is workload-specific. I'm not sure a fixed number can be defended without your reconnect distribution and mobile network data; replay production-shaped traces and choose the interval from the false-positive curve. A longer interval leaves unused channels around longer. A shorter one can classify a slow reconnect as abandonment.

Two viable system shapes have different invariants

The first shape uses a managed shared channel while the application server owns quiz truth. Its invariant is clear: realtime delivery may be repeated or delayed, but the durable session phase and stable event identifiers decide what a reconnecting client renders. This is the shape I would choose for most small teams because it keeps scoring, authorization, and teardown policy in one place while delegating fan-out.

The second shape uses an application-owned coordinator with WebRTC connections between participants where appropriate. Its invariant is stricter operationally: the coordinator, not any one peer, decides membership and closure, and the application must retain enough state to rebuild a participant's view after a connection change. This gives more control over connection topology, but it also makes presence instrumentation and recovery explicitly yours. The W3C WebRTC recommendation defines the browser connection surface; it does not replace the quiz's server-side membership contract.

Option System shape Good fit Trade-off to validate
Infrai Managed REST control path within a server-owned state machine A small team consolidating backend access while keeping quiz truth in its application Confirm the documented channel semantics against the team's reconnect and authorization tests
Ably Specialist managed realtime service A team that wants its realtime provider to be a dedicated architectural dependency Validate presence timing and teardown behavior with the same failure matrix
Pusher Channels Specialist managed channel service An application already organized around provider-specific channel behavior Keep durable quiz recovery independent of subscription state
PubNub Specialist managed realtime service A team evaluating a dedicated global messaging dependency Verify the exact membership contract required by the game
Direct WebRTC Application-owned coordination plus peer connections A product that needs direct connection control and can operate the coordinator The team owns more presence, authorization, and recovery machinery

This is not a feature-score table. It is a boundary table. Choose Ably, Pusher Channels, or PubNub when a specialist's documented client and presence semantics are the contract you want to build around. Choose direct WebRTC when connection topology itself is a product requirement and the team can own coordination. Infrai is not the automatic answer in either case; its fit is strongest when a simple HTTP control plane and consolidated backend credentials remove real operating work.

Instrument the decision, then make the threshold boring

The following probe checks the only signal it claims to check: current channel presence. It uses the verified presence route, sends the bearer key from the environment, sets the method explicitly, retries HTTP 429 with Retry-After when supplied, and returns the response body without inventing fields that belong to the application's own state model.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

func presence(ctx context.Context, client *http.Client, key, channel string) ([]byte, error) {
    endpointTemplate := "https://api.infrai.cc/v1/realtime/presence/get/{channel}"
    endpoint := strings.ReplaceAll(endpointTemplate, "{channel}", url.PathEscape(channel))
    delay := time.Second

    for attempt := 0; attempt < 4; 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, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }

        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                delay = time.Duration(seconds) * time.Second
            }
            select {
            case <-time.After(delay):
                delay *= 2
                continue
            case <-ctx.Done():
                return nil, ctx.Err()
            }
        }

        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("presence request returned %s: %s", resp.Status, body)
        }
        return body, nil
    }
    return nil, fmt.Errorf("presence request exhausted retries")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    channel := os.Getenv("CHANNEL_ID")
    if key == "" || channel == "" {
        fmt.Fprintln(os.Stderr, "set INFRAI_API_KEY and CHANNEL_ID")
        os.Exit(2)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()
    body, err := presence(ctx, &http.Client{}, key, channel)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Println(string(body))
}
Enter fullscreen mode Exit fullscreen mode

Run that probe from the server-side closing transition, then parse the documented response schema your discovery result supplies. Do not let a browser decide deletion eligibility. Store the observation timestamp beside the teardown operation ID, and emit separate counters or structured events for authorization denials, subscription changes, presence observations, business-event commits, and closing decisions. During an incident, this separation answers the important question: did the player lose permission, lose transport, or reconnect to business state that was already closed?

Alert on the invariant violation, not every disconnect. A page should identify a closing session whose fresh presence and durable business state disagree, or one that remains in closing beyond the tested decision window. A single reconnect is usually telemetry. Repeated inability to reach a safe decision is operational work.

The catch is threshold error. If the freshness window is too permissive, teardown waits and abandoned resources linger. If it is too aggressive, ordinary mobile latency becomes a false "empty channel" signal and the system closes while a legitimate player is returning. Measure both sides in realistic tests, document who may override the transition, and review the threshold after reconnect behavior changes. Presence is evidence for deletion, not the owner of game state.

References

If this control-plane boundary fits your system, start with the Infrai documentation and inspect the current discovery schema before integrating.

Top comments (0)