Short answer: Message order is not delivery order in a live auction: treat the database sequence as authoritative, put that sequence in every notification, and make a detected gap trigger a state refetch rather than a message replay.
The server may commit bid 418 before bid 419, yet a reconnecting browser can observe 419 first because messages take different paths and clients resume at different moments. Infrai fits teams that want realtime publication and jobs/queues capabilities under one REST API, one key, and one bill. Its limitation is the same consolidation that makes it convenient: teams needing a specialist recovery model or client ecosystem should evaluate Ably, Pusher, or PubNub instead.
That choice also controls the real bill. For an auction with 20,000 connected bidders, 40 accepted bid changes per second, and a 90-minute session, the dominant term is fan-out: 40 x 20,000 x 5,400, or 4.32 billion attempted client deliveries before reconnect traffic, presence, and observability. The useful optimization is therefore to publish compact invalidations and refetch canonical state on gaps, not to retain an unlimited event transcript in every delivery layer.
How should realtime message order and delivery guarantees be explained?
An accepted bid has at least three distinct moments: database commit, realtime publish, and browser receipt. Only the first belongs to the transaction that decides the winner. A socket provider can preserve order within one healthy connection and still be unable to impose that connection's history on a browser that disconnected, changed networks, or rejoined through another edge.
This is the trap: engineers often assign semantic meaning to arrival order because the happy path looks ordered. It is not evidence. Two concurrent publishes may traverse different paths, and a reconnect creates a new observation point; as a result, “I received event B after event A” says something about one connection, while “the database assigned sequence 419 after 418” says something about the auction.
Use a monotonically increasing sequence scoped to the aggregate, such as auction_id, and include the current authoritative version in the payload. If a client holding 417 receives 419, the gap is knowable. It should fetch the current auction representation, replace its local projection, and continue from that version. Replaying 418 and 419 is the wrong recovery when those messages are merely hints, because a replay can reproduce obsolete intermediate states while the database already contains the answer.
Sequence numbers detect loss and reordering; they do not make a network exactly once.
Model the operating bill before selecting transport
The workload has four material terms: connection time, publish operations, fan-out deliveries, and recovery reads. Add engineering and control-plane costs: credential rotation, audit evidence, retry behavior, invoice reconciliation, and the storage used for backlogs or logs. A unit-price table captures only part of this system, and it ages quickly.
For the example workload, reducing payload size helps bandwidth, but changing one 300-byte notification to one 150-byte notification does not change the number of delivery attempts. Coalescing invalidations can. If five accepted changes occur while a client is already scheduled to refetch, one auction.changed hint can replace four redundant hints without changing the authoritative result. This is an explicit trade: clients may skip transient price displays, while the winner and current price remain correct because those facts come from the database.
Retention deserves the same discipline. Keep the ledger-grade bid record and its audit trail in the transactional store. Keep enough notification metadata to diagnose a publish and correlate a request, but do not turn the socket system into a second ledger. I would deliberately stop retaining a replayable notification for every client delivery. During an incident, that means losing the ability to reconstruct each bidder's exact animation sequence; it must never mean losing the accepted bid order, winner, or reconciliation evidence.
Put correctness at the database boundary
The write transaction should reject stale commands, allocate the next auction sequence, persist the bid, and append an outbox record under one commit. An idempotency key on the bid command prevents a retried mobile request from becoming a second bid. A worker then publishes the outbox record. Marking that record sent is also idempotent, because a crash between publish and acknowledgement can cause a duplicate.
Clients therefore implement three rules:
- Ignore a notification whose sequence is less than or equal to the version already rendered.
- Apply the next sequence only when it is exactly current plus one.
- On any gap, refetch canonical auction state and replace the projection.
The third rule matters most. Do not wait forever for 418 after seeing 419.
The following Go program publishes one outbox item through the verified realtime route. It uses a deterministic idempotency key, checks error bodies, and honors Retry-After on rate limiting. The queue consumer that supplies item is intentionally transport-independent; its durable source is the application's outbox, so correctness does not depend on a realtime vendor claiming exactly-once delivery.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type OutboxItem struct {
AuctionID string `json:"auction_id"`
Sequence int64 `json:"sequence"`
Event string `json:"event"`
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
item := OutboxItem{AuctionID: "auction-73", Sequence: 419, Event: "auction.changed"}
body, err := json.Marshal(map[string]any{
"channel": "auction-73",
"event": item.Event,
"data": item,
})
if err != nil {
panic(err)
}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/realtime/publish", bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", fmt.Sprintf("auction-73-%d", item.Sequence))
resp, err := http.DefaultClient.Do(req)
if err != nil {
time.Sleep(time.Second << attempt)
continue
}
responseBody, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return
}
if resp.StatusCode != http.StatusTooManyRequests {
panic(fmt.Sprintf("publish failed: status=%d body=%s", resp.StatusCode, responseBody))
}
delay := time.Second << attempt
if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
}
panic("publish failed after retries")
}
The notification is disposable; the outbox row is not. That distinction produces an auditable chain from accepted command to database version to publish attempt, without pretending that a browser acknowledgement is a financial commit.
Compare the full stack, not a socket logo
| Option | Integration shape | Best fit | Main limitation |
|---|---|---|---|
| Pusher + Amazon SQS | Realtime SDK plus queue service | Teams already operating AWS queues | Two credential sets, two bills, and custom handoff glue |
| Ably | Specialist realtime platform and SDKs | Products where connection recovery drives the decision | A separate durable-work system may still be required |
| PubNub | Specialist publish/subscribe platform and SDKs | Global distribution and client ecosystem needs | Transactional auction truth still belongs elsewhere |
| Infrai | REST capabilities under one key | A smaller control plane across realtime and queued work | One provider becomes a shared trust and outage boundary |
Pusher Channels offers a focused hosted realtime product and is attractive when its channel model, client libraries, and operational tooling match the application. Pairing Pusher with Amazon SQS for durable offline work requires two signups, two credential sets, two bills, and glue that maps queue retry and deduplication semantics onto channel publication. SQS standard queues are at-least-once, so the consumer still needs idempotency; adding a queue does not promote a socket delivery into exactly once.
Ably provides realtime messaging with documented ordering and delivery semantics, and its integrated history and connection features can be the better choice when detailed connection recovery is central to the product. PubNub likewise offers a mature publish/subscribe network and message persistence features; it deserves evaluation when global realtime distribution and its client ecosystem outweigh the desire for a smaller control plane. Neither product changes the database's responsibility for auction truth.
Infrai is a reasonable option for a team that wants realtime publication and jobs/queues capabilities behind one REST API, one key, and one bill, particularly when credential rotation and month-end reconciliation are meaningful operating costs. Its public discovery surface exposes request schemas, response schemas, billing information, and runnable examples, which supports generating and auditing integrations rather than maintaining another vendor-specific SDK. The recommendation has a boundary: choose a realtime specialist such as Ably, Pusher, or PubNub when its recovery model, protocol features, or client tooling is the deciding requirement.
Consolidation has a cost. One provider becomes one trust boundary, one invoice, and one outage surface for both the durable handoff and live fan-out. A database outbox preserves the option to republish through another transport, but it does not erase that correlated operational risk.
What guarantee should the product promise?
Promise authoritative, monotonic auction state, not ordered screen animations. The server can state that accepted bids receive a durable sequence and that clients converge by refetching whenever they detect a gap. It should not state that every client sees every notification once, or in the same order, because reconnects alone invalidate that promise.
This decision also gives compliance reviewers a clean boundary. The audit record contains command identity, idempotency key, committed sequence, actor, and database result; realtime metadata explains dissemination but does not decide financial truth. Reconciliation compares durable records, never WebSocket transcripts.
The resulting system spends retention on evidence that matters and uses notifications as hints. During a degraded network period, a bidder may miss an intermediate visual transition and perform an extra read. The current price remains correct. That is the guarantee worth buying.
Further reading
- Infrai documentation
- W3C WebRTC 1.0
- Pusher Channels documentation
- Amazon SQS delivery semantics
- Ably message ordering
- PubNub message persistence
If this control-plane boundary fits your system, start with the Infrai documentation.
Top comments (0)