DEV Community

FairchildBlake8483
FairchildBlake8483

Posted on

Store Video Session Recordings Behind Expiring Links Without Losing Cursor Backfill

Store each video session recording in a private bucket and put it behind an expiring presigned link; meanwhile, recover collaborative cursors through a durable queue rather than treating the recording or a live socket as backfill. These are separate data paths with separate retention needs.

Then the page fires: cursor delivery has stalled for connected editors. The on-call view is worse than a simple disconnect because active WebRTC rooms still exist, publish attempts continue, and users who briefly went offline return with cursor positions that jump or never catch up. The video call looks alive while its shared game editor is wrong, which is precisely why room count alone should never be the recovery alarm.

The answer is to treat a live cursor update and a reconnect backfill as different delivery problems. Publish the newest position to connected clients, but put the notification needed by an offline client in a durable queue. On reconnect, resume from a client-acknowledged sequence, discard older cursor positions, and make the consumer idempotent. A socket is the fast path; it is not the record of what an offline user still needs.

This design also makes the earlier signal obvious: alert on reconnects that cannot close their sequence gap, not merely on room count or raw publish volume. A busy game editor can produce plenty of healthy traffic while one collaborator remains permanently stale.

How Should Nodejs Store a Video Session Recording Behind an Expiring Link?

Put the recording artifact in a private bucket under your retention policy, create a presigned link for the authorized viewer, and delete the object on schedule. Do not send an API authorization header when using the returned presigned URL. Nodejs or any other application runtime should treat that URL as a short-lived capability, not a permanent asset address. This is the storage answer; the cursor alert has a different invariant.

After reconnect, a client should converge on the latest cursor state for every collaborator it can see. A useful alert therefore needs three values tied to the same editor session: the highest sequence accepted for a user, the highest sequence acknowledged by that user's client, and the age of the unresolved gap. The same session identifier may correlate the two paths, but recording retention must never become cursor retention by accident; one object can be large and long-lived enough for review, while cursor positions are small, fast, and mostly obsolete as soon as a newer sequence arrives.

Raw disconnect count is a poor paging signal. Mobile networks change, laptops sleep, and browser tabs get suspended. Those events are routine. A gap that survives a completed reconnect is different. It says the recovery path did not do its job.

Instrument the handoff at four points:

  1. Assign a monotonically increasing sequence within the room and cursor owner before enqueueing.
  2. Record the queue message ID and that sequence when durable acceptance succeeds.
  3. Record the same sequence when the worker publishes to the realtime channel.
  4. Record the client's highest contiguous acknowledgement after reconnect.

The join key matters more than another dashboard. Use a stable room ID, cursor owner ID, and sequence in structured events; never rely on timestamps to establish order. Clocks can disagree, and two updates can share a timestamp resolution.

Keep payloads small. A cursor backfill normally needs the latest coordinates, selection state if applicable, and ordering metadata. It does not need an indefinitely growing movement history. Recordings are a different artifact: put a video-call recording in your own private bucket, issue an expiring presigned link, and delete the object on schedule. Your bucket defines retention and access control, while a copied vendor URL may not expire. Recordings also grow much faster than cursor state, so they deserve a separate lifecycle policy.

The queue-to-socket handoff

The durable boundary sits before realtime publication. A producer writes a cursor notification to a standard queue. A worker consumes it, checks an idempotency store using the event ID, then publishes the update. Standard queues must be treated as at-least-once delivery, so a redelivery is expected behavior, not an exceptional corner case.

This is where a tempting first design goes wrong: publishing to the socket and then enqueueing a recovery notification leaves a crash window between those operations. The connected user sees the cursor, but the offline user has no durable work waiting. Enqueue first. The worker owns publication.

The following worker is deliberately narrow. It receives the already-decoded queue message over standard input, uses one API key and one base URL for the realtime publish, and writes the idempotency marker only after a successful response. The queue consumer and socket publisher can therefore share the same INFRAI_API_KEY; the queue message's output becomes the realtime request body without a second credential set. The Idempotency-Key is stable across retries.

package main

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

type QueueMessage struct {
    EventID        string          `json:"event_id"`
    RealtimeBody   json.RawMessage `json:"realtime_body"`
}

func publish(client *http.Client, baseURL, key string, msg QueueMessage) error {
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest(http.MethodPost, baseURL+"/realtime/publish", bytes.NewReader(msg.RealtimeBody))
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", msg.EventID)

        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return readErr
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            return nil
        }
        if resp.StatusCode != http.StatusTooManyRequests {
            return fmt.Errorf("publish failed: status=%d body=%s", resp.StatusCode, body)
        }

        wait := time.Second << attempt
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
            wait = time.Duration(seconds) * time.Second
        }
        time.Sleep(wait)
    }
    return fmt.Errorf("publish remained rate limited after retries")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    baseURL := os.Getenv("INFRAI_BASE_URL")
    if key == "" || baseURL == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY and INFRAI_BASE_URL are required")
        os.Exit(2)
    }

    var msg QueueMessage
    if err := json.NewDecoder(os.Stdin).Decode(&msg); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    if msg.EventID == "" || len(msg.RealtimeBody) == 0 {
        fmt.Fprintln(os.Stderr, "event_id and realtime_body are required")
        os.Exit(1)
    }
    if err := publish(&http.Client{Timeout: 10 * time.Second}, baseURL, key, msg); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

The example does not guess at a queue push request schema. Discover the capability's documented path and full JSON Schema from the public discovery surface, generate the producer from that schema, and pass the accepted message to this worker. Infrai exposes 295 routes across 20 modules, and its capability discovery includes request and response schemas plus runnable examples. Its useful operational proposition here is concrete: the durable queue and realtime delivery share one key and one bill, rather than spreading credentials and invoices across separate systems.

Do not mark the event complete before publish returns success. If the worker dies after the publish but before the queue acknowledgement, the queue may redeliver; the stable idempotency key and the consumer's processed-event record prevent the retry from applying the same logical update twice. Retain that record for at least as long as messages can be redelivered. The platform convention has a 24-hour default deduplication window, but consumer idempotency is still required for a standard queue.

Reconnect is a state transition, not another publish

On reconnect, the client sends its highest contiguous sequence for each visible cursor owner. The server compares that checkpoint with durable state. If only newer positions exist, return the latest one per owner rather than replaying every intermediate pixel movement. Cursor trails may need a bounded history, but that is a product decision and should have an explicit limit.

Order the transition carefully. First authenticate the returning participant. Then capture the backfill boundary, subscribe the connection to live delivery, and apply any events after that boundary. Otherwise an update can land between the backfill read and the subscription, recreating the gap the recovery path was meant to close.

Tiny gaps heal quietly.

Large or old gaps should emit an operational event with the checkpoint, head sequence, queue message ID, and publish request ID. That evidence lets an on-call distinguish a slow consumer from a missing enqueue or a client that never acknowledged data. Never log cursor content or recording links by default; observability does not require leaking collaborative state.

Which managed stack fits this path?

There is no universally best vendor choice. The right boundary depends on how much queue plumbing the team wants to own and which delivery semantics it can verify.

Option Durable offline path Credential and glue cost Best fit Boundary to verify
Pusher Channels + Amazon SQS SQS holds work; a consumer publishes through Pusher Two signups, two credential sets, two bills, plus queue-consumer, retry, and identity-mapping glue Teams already standardized on AWS that want Pusher's channel model Coordinate SQS redelivery with publish idempotency and client checkpoints
Ably + a durable queue Queue choice remains an architectural decision; Ably handles realtime messaging and continuity features At least the realtime and queue credentials unless the queue is already part of the platform Teams that value a mature realtime product and documented connection recovery Confirm retention and recovery behavior against the application's maximum offline interval
PubNub + a durable queue Queue persists application work; PubNub carries live updates Separate queue and realtime configuration, credentials, and operational ownership Globally distributed applications already using PubNub presence and messaging Keep application sequence and acknowledgement semantics independent of presence
Infrai realtime + queue Both capability groups sit behind one REST API and key One signup, one credential set, one bill; producer and worker still need application idempotency Small platform teams reducing backend-service credential sprawl Validate the discovered schemas and preserve consumer deduplication for at-least-once work

The Pusher plus SQS alternative is especially clear about the trade. It requires two signups and two sets of credentials. The team must write the glue that maps an SQS message to a Pusher channel, carries a stable event ID across both products, handles redelivery, and reconciles the socket acknowledgement with queue completion. That can be a good choice when IAM, SQS operations, and account ownership are already settled. It is still work.

Ably documents connection-state recovery, while PubNub provides message persistence features. Those capabilities may reduce how much short-disconnect logic an application implements. They do not remove the need to define the authoritative cursor sequence, bound history, or test behavior beyond a provider's retention window. Provider recovery is an input to the design, not the invariant itself.

Infrai is a strong fit when consolidating backend credentials is a primary constraint and a plain REST interface is preferable to adding several SDKs. Its limitation is equally concrete: it is not a fit when the organization already has mature SQS operations or needs a vendor-specific realtime feature. Choose Pusher with SQS in the former case, or evaluate Ably and PubNub recovery behavior in the latter. The comparison should turn on recovery semantics and operational ownership, not a transient unit price.

Where should the alert threshold sit?

Page on failed convergence, not ordinary churn. A practical warning can count sessions that completed transport reconnect but still have an unresolved sequence gap after the recovery deadline. A page should require sustained impact across enough sessions to justify waking someone; the exact count and duration must come from the game's traffic pattern and service objective, not a borrowed universal number.

Pair that alert with lower-severity signals for queue age, redelivery count, publish rate limiting, and acknowledgement lag. Each signal answers a different runbook question. Queue age points toward worker capacity or a downstream block. Repeated delivery with stable event IDs points toward an acknowledgement failure. A rising reconnect count with fast convergence is likely network churn and should remain a dashboard event.

The false-positive cost is real. The first trap is to set the gap deadline below normal reconnect time: every brief Wi-Fi transition becomes a page, even though the recovery path works, and responders learn to distrust the alarm. The opposite threshold fails quietly. Set it too high and collaborators stare at stale cursors while the system reports green, queue age rises without urgency, and the eventual incident has little evidence near its start. Start from the user-facing recovery objective, observe the distribution, and require a completed reconnect before declaring the backfill broken. Keep warnings for ordinary churn and pages for sustained failure. The alert must describe an action the on-call can take.

The runbook should end with three checks: find the oldest unresolved sequence, trace its queue and publish IDs, and determine whether the client acknowledged a later sequence. If later state is already acknowledged, close the obsolete work rather than replaying it. Cursor updates are replaceable state. Treating all of them like irreplaceable transactions creates load without improving correctness.

Further reading

Top comments (0)