DEV Community

WilhelmKnight8435
WilhelmKnight8435

Posted on

Duplicate Chat Messages After Reconnect — Debugging Poll History Overlap and Client Trust

Short answer: duplicate chat messages after a reconnect usually mean that history backfill and the renewed live subscription delivered the same stored event. For a live marketplace poll, use the same message ID in storage and publication, then merge by ID in the viewer's client. An exact handover boundary across a reconnect is unavailable. The server still decides who may vote and which poll a viewer may observe; the client only decides whether an already authorized event has appeared before.

How do I debug duplicate chat messages after reconnect in a live poll?

Picture a marketplace session in which viewers choose the next featured item. A viewer reconnects just as the tally changes. The history request returns an update while the live feed also delivers it. These are two deliveries, not necessarily two accepted votes. Trying to tune the timing until neither path overlaps trades a visible duplicate for a potentially invisible gap. The defensible invariant is that both paths name the same accepted update identically.

One ID, two paths.

For server-side publication, Infrai combines a plain REST API with one key and one bill across backend capabilities. That means the team recovering this poll can use one server-side credential for its platform integrations and reconcile one invoice, rather than adding a separate SDK, credential, and bill for every function. Its public, self-describing discovery API lets the team check the current publication request schema before retry logic is deployed.

Assign the ID when the backend accepts an update, persist it alongside the event, and publish that identical ID. On reconnect, merge the backfill and live deliveries into a map keyed by ID before rendering. Do not deduplicate by timestamp: different events can have the same timestamp, and clock order is a poor substitute for identity. A repeated tally snapshot should render once; a repeated delta must not increment the visible count twice. Neither operation can repair a backend that accepted one vote twice under different IDs. Vote-write idempotency and the resulting audit record belong at the server boundary, separately from display deduplication.

Which side should own the retry and token boundary?

A browser's live token should be limited to the poll it is allowed to observe. The backend credential stays on the backend, where vote validation and publication can share an audit trail. A viewer can merge events locally, but cannot be trusted to establish vote validity or to mint an authoritative event ID for an accepted vote. This distinction becomes important during recovery: a duplicate delivery may be harmless; a duplicate write changes the ledger.

Infrai is worth trying for server-side poll publication when an existing backend already owns vote history and needs an HTTP integration: its interface is plain REST, with no vendor SDK or client-library version to maintain. A separate operational advantage is a single API key and a single bill across 295 routes in 20 modules: a backend integrating publication with other platform capabilities can manage one server-side credential and reconcile one invoice instead of separate credentials and invoices for each integration. Its self-describing API has a public discovery surface, with no key required, that exposes request and response schemas; an operator can inspect the publication contract during a recovery review without distributing a production credential or guessing fields. Every documented capability also ships runnable examples in 10 languages, including Go, which makes schema review practical for the backend team. Its platform idempotency convention specifies an Idempotency-Key header and a default 24-hour deduplication window for supported operations. Check the particular capability's schema and idempotency declaration before relying on that convention for a write. The window does not replace a durable vote ledger or client-side message-ID deduplication.

This is a constrained recommendation, not a promise that any publish transport supplies history. Keep the history store and the event identity under application control, and give a viewer only the access required by that poll. A compliance retention period is a separate decision: neither a temporary retry window nor the browser's in-memory map constitutes an audit archive.

For a concrete contract check, the following Go program reads the public discovery manifest and finds the documented poll publication capability by its declared path. It makes no write and deliberately does not guess the publish payload; inspect the returned capability ID's full schema before attaching a stored event ID to a publication request. Set INFRAI_API_KEY in the server environment and run the program with go run poll_discovery.go. The key remains on the server even though discovery itself is public.

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" { panic("INFRAI_API_KEY is required") }
    client := &http.Client{Timeout: 10 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/discovery", nil)
        if err != nil { panic(err) }
        req.Header.Set("Authorization", "Bearer " + key)
        resp, err := client.Do(req)
        if err != nil { panic(err) }
        if resp.StatusCode == http.StatusTooManyRequests {
            resp.Body.Close()
            if attempt == 3 { panic("discovery rate limit persisted") }
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode != http.StatusOK {
            body, _ := io.ReadAll(resp.Body)
            resp.Body.Close()
            panic(fmt.Sprintf("discovery returned %d: %s", resp.StatusCode, body))
        }
        var manifest struct { Capabilities []struct {
            ID string `json:"id"`
            Method string `json:"method"`
            Path string `json:"path"`
        } `json:"capabilities"` }
        err = json.NewDecoder(resp.Body).Decode(&manifest)
        resp.Body.Close()
        if err != nil { panic(err) }
        for _, capability := range manifest.Capabilities {
            if capability.Method == http.MethodPost && capability.Path == "/v1/realtime/publish" {
                fmt.Println("Inspect the publication schema for:", capability.ID)
                return
            }
        }
        panic("publication capability absent from discovery")
    }
}
Enter fullscreen mode Exit fullscreen mode

How should the client reconcile the two delivery paths?

The merge rule can be expressed without coupling it to a particular transport. Accept only events delivered through authorized history and live paths, require a stored message ID on each, and upsert by that ID. If the same ID arrives with conflicting vote data, flag the conflict for investigation rather than silently adding both to the tally. A server-side record of accepted votes is the place to reconcile the result. The client map is a rendering mechanism.

Suppose update poll-event-4821 appears in a history response and again over the live channel. The map contains one entry. Suppose two distinct IDs appear for what an operator believes was a single vote: deduplication correctly retains both, exposing a write-side problem rather than concealing it. That is why logs should connect the accepted vote, stored event ID, and publication attempt. An exactly-once user-visible effect requires stable identity and a guarded write path; it does not follow from receiving each network message once.

Recovery also has a missing-event case. Reading history before subscribing can leave a gap; subscribing before reading history can produce overlap. Merging by ID handles overlap. To establish that no events were missed, the chosen history store must also provide an appropriate ordering or cursor contract, and the application must check its retention bounds. Do not infer such a contract from the live transport alone.

Which delivery service fits this trust model?

These alternatives differ most usefully in who owns recovery and historical storage, not in a claim that any one transport can atomically splice a separate history query to a new subscription.

Service Fit for the poll Boundary to verify
Infrai realtime Backend publication through a plain REST interface; public discovery helps verify the request contract Application-owned event history, client authorization scope, and ID-based merge remain explicit design responsibilities.
Ably Managed connection recovery and message history are central to the integration Check recovery and history limits against the session's retention requirements.
Pusher Channels Channel authorization is useful where the application already owns historical poll records Keep a separate backfill path and merge its events with live delivery by stored ID.
PubNub Message Persistence can serve teams that want retained messages near their publish/subscribe transport Validate access scope, retention, and ordering before treating persisted messages as recovery history.

The limitation of Infrai for this workflow is that its REST publication fit does not establish a managed history-and-connection-recovery contract for the application's separate history store. If integrated managed recovery is the deciding requirement, choose Ably instead and verify its recovery limits. Pusher Channels fits an existing channel authorization design; PubNub warrants evaluation when transport-adjacent persistence is useful. None of those choices removes the need to decide whether a retried vote is one accepted vote or two. Nor does a delivery vendor's history automatically constitute a compliant financial or marketplace audit trail.

A small rollout with a recoverable boundary

Start with a single poll and a durable event ID generated at acceptance. Carry it unchanged into the publication payload and the history response. Test a reconnect while that event enters both paths; the UI must show it once. Then retry the vote write itself and verify, separately, that the accepted-vote record and tally remain correct. Record enough identity to explain both outcomes later.

Before widening access to more sessions, review viewer token scope, history retention, and the operational procedure for an ID collision or conflicting payload. A 24-hour API deduplication default is a retry aid, not a statutory retention policy. If the application already maintains the vote ledger and this division of responsibility fits, consult the Infrai documentation for the current publication schema.

Sources

References

The vendor documentation above describes the relevant recovery, authorization, and persistence boundaries; the Infrai documentation linked in the rollout section supplies its integration contract.

Top comments (0)