QUESTPIE
ClientChannels

Delivery

What a channel promises about the events it carries. Ordering per resolved channel, replay after a reconnect, an explicit gap when replay cannot reach far enough, and hard caps on payload and buffer size.

View markdown

Can you trust the order? Yes, per resolved channel, and the same on both transports. You cannot trust that every event survives an outage. The runtime says so out loud rather than guessing.

Event ids

Every appended event gets an id built from the resolved channel name and a counter.

7f3a…c21b:42

The left half is a SHA-256 hash of the resolved channel. The right half is a sequence that starts at 1 and never skips. chat-room-a and chat-room-b count independently, because they resolve to different names.

The client keeps a cursor per channel and compares each arrival against it.

ArrivalWhat happens
Next in sequenceDelivered to your callback
Already seenDropped silently. A reconnect may repeat an event
Ahead of expectedThe subscription errors and closes

Events are never coalesced. A live query collapses to the latest snapshot. A channel does not, because two chat messages are not two versions of one thing.

Replay and gaps

Appended events are kept so a reconnecting client can catch up from its last applied id. The window is bounded on both time and size. An event goes when either bound is crossed.

OptionDefaultWhat it sets
retentionMs24 hoursAge of a replayable event
retentionBytes64 MBTotal retained payload across channels
batchSize500Rows read in one channel-local batch
busyRetryMs25Retry delay behind a busy local sink

Set either retention bound to 0 to switch that half of the cleanup off. The options live under realtime.channelEvents.

When a client asks to replay from an id older than the window, the server answers with a gap. The subscription fails with Channel event replay gap instead of resuming mid-stream. Recover by reading from a collection, or by subscribing again from now and accepting the hole.

A gap is a signal, not a failure to hide

The runtime never invents the events it lost. Handle the error in onError, decide what your screen should show, and resubscribe on purpose.

Knowing when you are caught up

subscribe() takes a third argument, { signal, onError, onReady }. onReady fires once the server has authorized that subscription and replay has run dry.

client.channels.chatRoom.subscribe({ roomId }, render, {
	onReady: () => setCaughtUp(true),
});

Replayed events reach your callback before onReady does. So it marks the line between catch-up and live, not the first frame.

It fires once per admission, and a reconnect starts a new one. So expect it again after every recovered drop. A subscriber that joins a channel already caught up gets it on the next microtask, and its own events wait behind that call.

subscribePresence() takes onReady too, and it holds every roster until that callback has run. The one-shot presence() does not take it. That call resolves with a roster and ends.

Size limits

The canonical { eventId, event, data } envelope must fit 10,000 UTF-8 bytes. That is the whole frame. Your data budget is a little under 10,000. It shifts with the length of the event name. A publish over the limit is refused with 413, and no event is stored.

data must also be JSON-serializable. A value that cannot be stringified is refused with 403.

Slow consumers

Queues sit at two seams, one behind a local server sink and one in the client's async iterator. Both are bounded by count and by serialized bytes.

SeamEventsBytes
Server sink1001 MiB
Client iterator1001 MiB

Overflow terminates that consumer. The client iterator throws Channel client slow consumer. The server closes the subscription with reason slow_consumer. Ordered events are never dropped quietly to keep a slow reader alive. Tune the server side with maxBufferedEvents and maxBufferedBytes.

If a for await loop is falling behind, the work inside it is too heavy. Push the message onto your own structure and process it elsewhere.

Dates survive the trip

Ordered events, replay and presence all preserve nested Date values. The wire format carries an explicit list of the paths that hold a date and revives exactly those. No string that happens to look like a timestamp gets converted by accident. See Temporal values.

Next

Reactive apps puts these limits in context. It covers which reactive primitive to reach for and how to size a channel.

On this page