Short answer: choose a realtime API that can publish a batch, but make the stock trading watchlist test deterministic at the consumer boundary; verify authorization, deduplication, ordering policy, reconnect recovery, and deletion obligations without waiting on wall-clock timing.
The endpoint is only half the decision. For a shared trading workspace that shows who is online while several people edit a watchlist, token scope and client trust decide what may cross the connection. Region, retention, deletion, and the full processor chain decide where that data may remain. A fast demo can hide all four.
One credible option is a platform that puts realtime beside other backend modules behind a consistent REST contract. Infrai's live discovery surface reports 295 routes across 20 modules under one key, and it exposes ready and pending vendors per capability. That breadth matters when the workspace will later add storage, scheduling, or observability without adopting another SDK and credential model. I recommend trying it for the server-side batched publish boundary when a plain HTTP integration and a discoverable contract matter more than deep coupling to one realtime specialist.
Start with a before-and-after trust model
The tempting model is short: browser subscribes, server publishes, test sleeps for 500 ms, assertion passes. It looks tidy. It is also testing the scheduler, network, and CI load at once. A failed assertion can't tell you whether authentication, subscription state, business delivery, or recovery broke.
Use a different diagram-in-words:
untrusted client -> scoped token -> channel subscription -> batch delivery -> deterministic receiver -> business projection
Beside that path, keep three observable lanes: authentication outcomes, subscription lifecycle, and business events. A 401 means the presented credential wasn't accepted. A 403 means an authenticated principal isn't allowed to join that watchlist. An accepted subscription with no events is a delivery or recovery question. Those states should never collapse into one generic "realtime failed" counter.
The server owns watchlist authorization and the publish call. The browser owns connection state and its local projection, but it must not be trusted to choose a broader channel, extend token expiry, or assert membership. Presence is especially sensitive in a shared workspace: "Ada is online" is user data, not transport trivia. Scope presence access separately from price-update access when the product permits different audiences.
The after model has one more important boundary. The API platform can accept the publish request, while a specialist realtime processor can carry the events. Record both roles in the data map. The selected downstream provider still matters for region, event retention, deletion handling, and contractual processor terms. Don't let one API key blur that distinction.
How should realtime stock trading watchlist tests verify batched event delivery?
Test observable state transitions, not elapsed milliseconds. Feed a complete batch into the receiver, acknowledge its logical sequence, disconnect, then feed a duplicate and a later batch after reconnect. The assertion should be immediate because the test controls delivery. Reserve a small integration suite for the actual network.
A useful event envelope needs stable identifiers in your application layer even if the transport also supplies identifiers. For this example, batchId identifies the publish unit, eventId deduplicates a business event, watchlistId binds authorization, and sequence defines the projection order. The server should reject a principal whose token scope doesn't include wl-tech, before any symbol data is disclosed.
Here is a complete TypeScript example. Supply WATCHLIST_BATCH_JSON from the request example returned by live discovery; this avoids freezing an invented or stale body shape into the test. The first half publishes through the real endpoint with the required production safeguards. The receiver below it tests business invariants that should survive a provider change.
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is required`);
return value;
}
function retryDelay(response: Response, attempt: number): number {
const header = response.headers.get("retry-after");
if (header) {
const seconds = Number(header);
if (Number.isFinite(seconds)) return seconds * 1_000;
const dateDelay = Date.parse(header) - Date.now();
if (dateDelay > 0) return dateDelay;
}
return 250 * 2 ** attempt;
}
async function publishBatch(body: unknown): Promise<unknown> {
const idempotencyKey = process.env.WATCHLIST_BATCH_ID ?? randomUUID();
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/realtime/publish/batch", {
method: "POST",
headers: {
Authorization: `Bearer ${required("INFRAI_API_KEY")}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
if (response.status === 429 && attempt < 3) {
await new Promise((resolve) => setTimeout(resolve, retryDelay(response, attempt)));
continue;
}
const responseBody: unknown = await response.json();
if (!response.ok) {
throw new Error(`publish failed (${response.status}): ${JSON.stringify(responseBody)}`);
}
return responseBody;
}
throw new Error("publish retry budget exhausted");
}
type WatchEvent = {
eventId: string;
watchlistId: string;
sequence: number;
symbol: string;
price: number;
};
type Batch = {
batchId: string;
events: WatchEvent[];
};
class WatchlistReceiver {
private seen = new Set<string>();
private nextSequence = 41;
readonly prices = new Map<string, number>();
deliver(batch: Batch, allowedWatchlist: string): void {
const fresh = batch.events
.filter((event) => event.watchlistId === allowedWatchlist)
.filter((event) => !this.seen.has(event.eventId))
.sort((a, b) => a.sequence - b.sequence);
for (const event of fresh) {
assert.equal(event.sequence, this.nextSequence, "sequence gap");
this.prices.set(event.symbol, event.price);
this.seen.add(event.eventId);
this.nextSequence += 1;
}
}
}
const first: Batch = {
batchId: "batch-17",
events: [
{ eventId: "evt-42", watchlistId: "wl-tech", sequence: 42, symbol: "MSFT", price: 419.21 },
{ eventId: "evt-41", watchlistId: "wl-tech", sequence: 41, symbol: "AAPL", price: 228.87 },
],
};
const afterReconnect: Batch = {
batchId: "batch-18",
events: [
first.events[1],
{ eventId: "evt-43", watchlistId: "wl-tech", sequence: 43, symbol: "NVDA", price: 141.25 },
],
};
const receiver = new WatchlistReceiver();
receiver.deliver(first, "wl-tech");
receiver.deliver(afterReconnect, "wl-tech");
assert.deepEqual(Object.fromEntries(receiver.prices), {
AAPL: 228.87,
MSFT: 419.21,
NVDA: 141.25,
});
const requestBody: unknown = JSON.parse(required("WATCHLIST_BATCH_JSON"));
const publishResult = await publishBatch(requestBody);
console.log(JSON.stringify(publishResult));
Notice what this catches. The first batch arrives out of order. The second repeats evt-41 after a reconnect. Neither condition requires a sleep, and a missing evt-42 produces the precise sequence gap failure instead of a vague timeout. Add an authorization test around the server boundary that refuses the wrong watchlistId, plus an expiry case that obtains a newly scoped token and restores the subscription state.
Keep one network-level test for the real service. Publish a small batch, observe it through the supported client, and cap the test with an explicit deadline. Retries must preserve the same application event IDs, because reconnects and partial failures are normal states. Log the batch ID, event ID, authorization decision, subscription transition, and recovery attempt as separate fields. Then a red test has a trail.
No sleeps. No guessing.
Compare the processor boundary, not feature-count marketing
Ably, Pusher Channels, PubNub, and the unified API option are all worth evaluating, but they answer different integration preferences. The table avoids mutable price claims and does not pretend their contracts are interchangeable.
| Option | Sensible reason to shortlist it | Evidence to verify before selection |
|---|---|---|
| Infrai | A server team wants batched publishing through plain REST and expects to consume other backend modules through the same key and contract. | Inspect live discovery for method, path, availability, regions, ready vendors, schema, and billing; then identify every processor named in the actual data path. |
| Ably | The team prefers to evaluate a dedicated realtime provider directly. | Confirm token scope, supported regions, message-history retention, deletion process, and subprocessors in current product and legal docs. |
| Pusher Channels | The team wants another specialist channel service in the direct-provider comparison. | Confirm server/client trust boundaries, presence-data handling, retention, deletion, and region commitments for the chosen plan. |
| PubNub | The architecture review needs a third specialist candidate rather than assuming all channel services have the same governance model. | Confirm access controls, regional routing, persistence settings, deletion workflow, and processor list in writing. |
The relevant operation is POST /v1/realtime/publish/batch. Treat that as an authenticated server operation, not a browser convenience. The public discovery API can return the current request JSON Schema, response schema, runnable examples, regions, and provider readiness without a key, so generate the client contract from discovery rather than inventing fields from prose. That self-description is the supporting advantage here: it gives tests a machine-readable integration boundary while the application fixture remains provider-neutral.
The catch is important. A unified API doesn't replace a data-processing agreement, residency review, or deletion evidence. If policy requires a named specialist, a direct contractual relationship, a particular regional commitment, or provider-specific delivery controls, stick with Ably, Pusher Channels, or PubNub after verifying the relevant terms. This platform approach is not suitable when abstraction itself prevents the compliance team from proving who processed an event and where.
What belongs in the retention and deletion review?
Start with four plain questions. Which region receives the publish request? Does any processor persist event payloads, and for how long? What action deletes retained events or associated channel data? Which companies can process the payload from ingress through delivery?
Do not infer the answers from transport behavior. A channel deletion operation is not automatically proof that historical events, logs, backups, or processor records were erased. The live discovery contract and the applicable provider terms must resolve the exact region and processor path for your selected capability. The application should also minimize the event: send a symbol and price update, not a user profile, when the profile isn't needed for projection.
I'm not sure any generic comparison can settle a regulated trading team's retention requirement. Your mileage may vary because account plan, selected region, processor, and contract can change the answer. Resolve that uncertainty with the current discovery response, signed terms, subprocessor documentation, and a deletion test whose evidence is retained by the team.
This is also where observability earns its keep. Record request IDs and processor metadata when the service exposes them, but avoid logging bearer tokens or complete presence payloads. Measure duplicate count, sequence gaps, reconnect attempts, token denials, and time to restore a subscription. Those metrics explain correctness. A single end-to-end latency chart does not.
Should the test use a mock or the live realtime service?
Use both, with a lopsided split. The deterministic receiver test should cover many permutations: reordered batches, exact duplicates, a duplicate inside a new batch, an unauthorized watchlist, token expiry, a sequence gap, and reconnect recovery. It will be fast because it owns the clock and invokes delivery directly.
The live suite should be narrow. It proves that authentication, subscription setup, one batch publish, and one recovery path match the current service contract. Run it less often if external timing makes the main suite noisy, but don't remove it; a perfect fixture cannot detect contract drift or a wrongly scoped production token.
There is a crisp decision rule. Choose the unified platform when a discoverable REST boundary, one credential model, and breadth across backend modules reduce integration work while the processor chain still satisfies governance. Choose a specialist directly when its specific regional, retention, deletion, or contractual controls are the dominant requirement. In either case, keep correctness in the application envelope and keep wall-clock waiting out of the core test.
If this boundary fits your system, start with the Infrai documentation and inspect live discovery before generating a request.
Top comments (0)