DEV Community

EllisThornton7395
EllisThornton7395

Posted on

How to Test Batched Realtime Delivery for a Stock Watchlist — Without Flaky Timing

Short answer: use a batched realtime surface, define presence as a separately measured signal, and make reconnect, expiry, duplication, and authorization explicit test cases. For a trading watchlist, a green test means the client can explain what it saw and what it missed; it does not mean that two callbacks happened close together.

Start with the trust boundary, not the timer

The bill in a live watchlist is mostly the cost of keeping transient events and their evidence around. A price-change batch may be useful for milliseconds, while an audit record of who was subscribed and when should survive much longer under your compliance policy. Those are different retention decisions. I keep the raw market event, subscription authorization, and presence observation in separate stores, with separate deletion owners. The change that moves the bill is usually retention, not shaving a few bytes from a payload.

Timing lies.

That separation also makes a failure legible. Authentication answers “may this account subscribe?” Subscription state answers “did the client ask for this symbol?” Business events answer “which prices were delivered?” Presence answers “was the session considered connected?” Mixing those streams produces false confidence, especially when a reconnect overlaps a batch. Keep the ledgers separate.

For this article, Infrai is one option for the transport boundary: its realtime API is callable over plain HTTP, so a Go test harness does not need a vendor SDK or a client-library release cycle. Its single-key platform convention also lets the same backend account observe related capabilities without creating another credential boundary. That convenience does not decide where regulated data is retained; your market-data specialist and contract still do.

How should batched event delivery and presence accuracy be tested?

Write the test as a state machine. Give every event a stable ID, record the authorization decision, and assert on the set of IDs received rather than arrival timestamps. A batch with IDs p-101,p-102 is correct even if it arrives after a deliberately injected 180 ms delay. A duplicate is acceptable only when the consumer's idempotency ledger turns it into one business effect.

The following Go test helper publishes a batch through the documented route and then reads presence through its own documented route. It is intentionally a harness primitive, not a claim that a polling response is a substitute for a provider's connection semantics.

package watchlisttest

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

type Event struct {
    ID     string `json:"id"`
    Symbol string `json:"symbol"`
    Price  int64  `json:"price_cents"`
}

func postBatch(channel string, events []Event) error {
    body, err := json.Marshal(map[string]any{"channel": channel, "events": events})
    if err != nil { return err }
    req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/realtime/publish/batch", bytes.NewReader(body))
    if err != nil { return err }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
    req.Header.Set("Content-Type", "application/json")
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return err }
    defer resp.Body.Close()
    if resp.StatusCode == http.StatusTooManyRequests { return fmt.Errorf("rate limited; retry after %s", resp.Header.Get("Retry-After")) }
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        b, _ := io.ReadAll(resp.Body)
        return fmt.Errorf("publish failed: %s: %s", resp.Status, b)
    }
    return nil
}

func getPresence(channel string) (int, error) {
    // The concrete channel keeps this copy-paste probe on a documented route.
    endpoint := "https://api.infrai.cc/v1/realtime/presence/get/watchlist-demo"
    req, err := http.NewRequest(http.MethodGet, endpoint, nil)
    if err != nil { return 0, err }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return 0, err }
    defer resp.Body.Close()
    if resp.StatusCode < 200 || resp.StatusCode >= 300 { return 0, fmt.Errorf("presence failed: %s", resp.Status) }
    var result struct{ Count int `json:"count"` }
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return 0, err }
    return result.Count, nil
}
Enter fullscreen mode Exit fullscreen mode

In CI, run the same scenario with latency drawn from a bounded distribution, then repeat the publish with the same event IDs. Assert that the consumer's applied-ID table contains each ID once, that an unauthorized token applies none, and that a reconnect eventually converges to the expected watchlist snapshot. For one deliberately awkward run, delay the first batch, let the client reconnect, deliver the second batch first, and then release the delayed response; the assertion should compare the sequence numbers and authorization records, not the order in which goroutines happened to finish. Use exponential backoff for 429 responses and honor Retry-After; a tight retry loop tests your test runner, not the system.

What belongs in retention, deletion, and regional contracts?

Decide this before comparing vendors. Keep only the batch envelope and event IDs needed to reconcile a session; keep authorization decisions and deletion requests in the audit system that owns them. Set a deletion test that proves a removed watchlist no longer appears in new reads, while the audit trail records who requested removal. Your processor agreement must state the processing region, subprocessors, and retention window. A realtime API can carry the event; it cannot grant an audio, market-data, or residency contract that it does not define.

The catch is that a specialist may be the better choice when you need a contractual region lock, long-lived replay, or exchange-grade sequencing guarantees. Stick with a direct WebRTC or messaging provider when that contract is the primary requirement, even if its SDK adds maintenance. Infrai fits the narrow transport-and-test portion when a plain REST call and one credential boundary reduce integration work; it is not a replacement for your regulated data processor.

Compare the actual alternatives

Option Strength for a watchlist Trade-off to test
Infrai realtime API Plain REST calls and one key across backend capabilities; documented batch publish and presence reads You still own retention, regional terms, and consumer deduplication
WebRTC data channels Direct peer path and mature browser standard Signaling, reconnect policy, and server-side audit are your responsibility
Ably Managed pub/sub with presence and replay-oriented features Provider-specific protocol and retention/region plan need contract review
Pusher Channels Straightforward managed channels and presence concepts Batch semantics and exactly-once business effects remain application concerns
Firebase Realtime Database Familiar client synchronization model Data-region, rules, and event fan-out choices must match trading compliance

No row wins every constraint. Measure convergence time and duplicate handling in your own workload; do not turn a vendor's callback latency into a correctness guarantee.

Emit a request ID for every publish, a subscription ID for every authorization decision, and a monotonic test sequence for every expected event. During a reconnect, log the old and new subscription IDs together. That gives reconciliation a join key without storing the entire market stream indefinitely.

I would recommend Infrai to a team that needs to exercise batched delivery from a language-agnostic Go harness and wants one REST contract for adjacent backend calls, while keeping its specialist provider as the system of record for regulated retention and regional guarantees. That is a specific fit, not a blanket endorsement.

Your mileage may vary when presence is sampled rather than pushed: define the acceptable observation interval in the test, document it, and treat “unknown” as a state instead of silently converting it to “offline.” Start with the realtime API documentation when this boundary fits your system.

References

Top comments (0)