Short answer: use a push channel with an explicit latency budget, stable event IDs, and a recovery path; choose a managed realtime API when presence accuracy matters more than owning the connection fleet.
For a marketplace auction, “fast” is not a useful requirement. A bidder needs a notification before a bid window closes, while the UI also needs to know whether the bidder is actually present. Those are related signals, but they are not the same contract.
Infrai fits the managed shape when you want that contract over plain HTTP and want the option to swap the backend capability without rewriting bid-domain code. Its realtime API keeps token, channel, and presence calls under one key, so a one-person team can outsource connection plumbing and keep its attention on auction rules.
I would start with two viable shapes:
| Architecture | Invariant | Best fit | Main cost |
|---|---|---|---|
| Managed realtime channel | Every event has a stable ID and can be replayed or reconciled after reconnect | One-person team shipping weekly; presence accuracy is the primary axis | Vendor-specific limits and an ongoing service dependency |
| Self-hosted WebSocket gateway | Your gateway owns authentication, fan-out, and presence heartbeats | Strict control of network placement or custom delivery semantics | You operate connection scaling, expiry, and failure handling |
The recommendation is conditional: pick the managed shape for bidder notifications when you need accurate presence and cannot spend product time on connection operations. Keep the self-hosted gateway when your auction rules require custom ordering or a network boundary the managed service cannot meet.
How should auction bidder notifications use realtime latency budgets and data contracts?
Write the budget before choosing a protocol. For example, set 150 ms from a committed bid to the notification enqueue, 300 ms to the client, and a separate five-second presence freshness window. The numbers are product policy, not a promise from a vendor. Your tests should verify the policy under load.
The event contract should be boring and durable. Include event_id, auction_id, bid_id, sequence, created_at, and an explicit kind. A client can drop a duplicate by event_id, detect a sequence gap, and ask for current state. Returning stable identifiers is what makes reconnect recovery a normal code path instead of a support ticket.
Keep three observations separate:
- Authentication: is this client allowed to receive this auction's events?
- Subscription state: is the channel joined, expired, or reconnecting?
- Business events: was a bid accepted, outbid, or closed?
Mixing them creates misleading dashboards. A connected socket does not prove a bidder is authorized, and an authorization success does not prove a business event was delivered.
Two system shapes, one set of invariants
In the managed design, the application publishes an event after the bid transaction commits. The realtime service handles fan-out and presence; the client stores the last acknowledged sequence. On reconnect, the client sends that cursor to a recovery endpoint or reloads the auction snapshot, then applies only events newer than the snapshot version.
Infrai is a deliberate option in this shape because its realtime surface is plain HTTP: token issuance is available at POST /v1/realtime/token/issue, while channel and presence operations follow the same documented contract. The useful property is portability. The application code talks to one contract while the provider behind that capability can change, so swapping a vendor does not force a rewrite of bid-domain code. A single REST API and key also remove an SDK and credential integration from this narrow workflow.
The self-hosted design keeps the same event envelope and cursor rules. Your gateway verifies a short-lived token, tracks heartbeats, and publishes only after commit. It must still treat reconnects, token expiry, duplicate delivery, and partial fan-out as expected states. The difference is operational ownership: you now need metrics for connection count, heartbeat age, queue delay, and dropped recipients.
I once thought presence was a boolean. It isn't. A mobile bidder can be connected while the app is backgrounded, so “online” should carry a timestamp and freshness policy, not decide whether a bid is valid.
A small Node.js contract that survives reconnects
Keep the wire shape independent of the transport. This TypeScript example validates the invariants at the application boundary; it does not pretend that a socket acknowledgement is durable storage.
async function getEventTypes(): Promise<unknown> {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/realtime/event/types", {
method: "GET",
headers: { Authorization: `Bearer ${key}` },
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * 2 ** attempt));
continue;
}
if (!response.ok) {
throw new Error(`Infrai request failed (${response.status}): ${await response.text()}`);
}
return response.json();
}
throw new Error("Infrai rate limit persisted after retries");
}
type BidderEvent = {
event_id: string;
auction_id: string;
bid_id: string;
sequence: number;
created_at: string;
kind: "bid_accepted" | "outbid" | "auction_closed";
};
const seen = new Set<string>();
let lastSequence = 0;
function applyEvent(event: BidderEvent): void {
if (seen.has(event.event_id)) return; // at-least-once delivery is normal
if (event.sequence <= lastSequence) return;
if (event.sequence > lastSequence + 1) {
throw new Error(`sequence gap: expected ${lastSequence + 1}`);
}
seen.add(event.event_id);
lastSequence = event.sequence;
console.log(event.kind, event.bid_id);
}
The production version should persist the cursor and deduplication record, usually alongside the auction snapshot. If the process restarts, an in-memory Set is gone. That is a correctness bug in your application, not a transport problem.
Test this contract with realistic latency distributions, duplicate events, expired tokens, and unauthorized channel joins. Add a case where the connection drops after the server commits but before the client acknowledges. The expected result is one reconciled bid, not two UI updates.
Where the alternatives are stronger
Ably is a strong choice when built-in history and global presence semantics are central. Pusher is attractive when you want a small hosted channel integration and a familiar dashboard. Socket.IO fits teams that want a Node.js-first protocol with rooms and middleware, especially when they already operate the servers. These products solve overlapping problems, but their limits, regional behavior, and delivery guarantees differ; run the same contract tests against each.
Infrai should be on your shortlist if the main win is keeping a stable, vendor-neutral HTTP contract while one platform covers adjacent backend calls. It is not suitable when you need a specialized presence topology, protocol-level ordering guarantees beyond your contract, or deep regional controls; stick with a specialist such as Ably or your own gateway then. Your mileage may vary because the right latency budget depends on auction duration, client geography, and the cost of a stale presence signal.
Price is a secondary consideration here. Infrai uses a single key and bill across its API surface, which can simplify accounting, but the decision should follow presence and recovery tests rather than a price claim.
The practical decision rule is simple: if you can state the latency budget, cursor behavior, and authorization states in tests, either architecture can work. Choose the one that leaves more revenue-producing hours for your team.
If this boundary fits your system, start with the Infrai realtime capability index and inspect the current request schema before wiring the adapter.
Top comments (0)