QUESTPIE

Tuning

Every key on RealtimeConfig, the admission limits that reject a topic before it costs anything, and the outbox table the whole runtime reads from.

View markdown

Override what you have measured. Each default below was picked to work on an untuned server, and most apps ship without touching any of them.

RealtimeConfig

import type { RealtimeConfig } from "questpie/realtime";
OptionDefaultWhat it controls
nativeDeltasfalseKeyed row deltas. While off, qualifying topics are served as snapshots
rowLiveQueriestrueApp-wide switch for collection and global row topics
subscriptionScopenoneServer-owned principal scope folded into scheduler identity. A non-null result over 256 UTF-8 bytes throws
observernoneLifecycle, admission and transport events. Observer failures never break delivery
admissionsee belowPer-session topic, connection and query-shape caps
connectionAcceptPacingMsnoneRandom delay before accepting a new edge session, itself clamped to 30000 ms
changeBrokerPgNotifyChangeBroker when a pg connection string existsThe cross-instance notice seam
clientTransportSSEThe edge delivery seam
channelEventssee belowOrdered-channel retention, lease and buffer tuning
channelPresencesee belowSSE presence leases and convergence
channelSecuritysee belowTrusted origins, publish rate, authorization deadline
pollIntervalMs15000 with push, 2000 withoutReconciliation interval. 0 gives up that recovery path
batchSize500Maximum outbox rows read per drain
retentionDays3Minimum time a row remains after it first becomes drainable. 0 disables cleanup
keepAliveIntervalMs8000Ping interval on each SSE stream

The poll runs alongside a broker rather than instead of it, and a provider failure temporarily tightens it to at most 2000 ms. Cleanup never uses a process-local cursor: it first marks the fleet-wide prefix below PostgreSQL's settlement frontier, then retains that prefix for retentionDays. A transaction held open for days therefore delays cleanup instead of letting cleanup erase committed rows before any instance is allowed to drain them.

Keep `keepAliveIntervalMs` under your idle timeout

8000 is deliberately under Bun's 10s default idleTimeout, so streams survive an untuned Bun.serve. Behind a proxy with a shorter read timeout, lower it further.

Admission

The edge rejects an abusive or accidentally unbounded topic before it registers a listener or computes a snapshot.

LimitKeyDefault
Topics per connectionmaxTopicsPerConnection20
Connections per authenticated principalmaxConnectionsPerPrincipal5
find limit, also applied when omittedmaxFindLimit100
Nested with depthmaxWithDepth3
Concurrent initial snapshotsinitialSnapshotConcurrency4
Buffered snapshot bytes per sessionmaxBufferedSnapshotBytes1 MiB
realtime: {
	admission: { maxTopicsPerConnection: 30, maxFindLimit: 50 },
}

A topic over maxFindLimit is rejected, not clamped and not split, because either would change ordering, pagination and result completeness behind your back. Raise it only after measuring snapshot compute time, serialized bytes and slow-client behavior for the real query shape. A large paginated read model is better served by ordinary pagination or a purpose-built projection than by one enormous live snapshot.

Rejections arrive as REALTIME_TOPIC_REJECTED carrying the topic id, resource, operation, retryable: false, and the requested and configured limits. Query filters are never included. The error reaches only that topic, and the TanStack adapter surfaces it as an errored query without retrying.

Delta topics have their own caps, maxDeltaFindLimit at 384, capped further by the bootstrap capacity that maxBufferedSnapshotBytes and estimatedDeltaRowBytes imply, plus maxBufferedDeltaEvents at 512, maxBufferedDeltaBytes at 1 MiB, deltaHydrationConcurrency at 4, and deltaRebootstrapIntervalMs at 60000.

Channel events and presence

channelEvents tunes the ordered ledger: retentionMs 24 hours, retentionBytes 64 MiB, maxBufferedEvents 100, maxBufferedBytes 1 MiB, coordinatorLeaseMs 30000, busyRetryMs 25, batchSize 500. Set either retention value to 0 to disable that half of cleanup.

channelPresence covers SSE presence only, since Pusher and Soketi manage their own: leaseMs 30000, heartbeatMs 10000, reconciliationMs 1000, and maxMembers 100 per resolved channel for provider parity.

channelSecurity takes trustedOrigins, authorizationTimeoutMs at 5000, publishRatePerSecond at 10 and publishBurst at 20. A serialized channel payload over 10000 bytes is refused.

The outbox table

One Drizzle table, questpie_realtime_log. QUESTPIE folds it into your app's schema unconditionally, so your normal migration creates it. You rarely touch it, but the whole runtime reads from it.

ColumnNotes
seqbigserial primary key, allocated when the row is inserted, not when its transaction commits
txidxid8, defaults to pg_current_xact_id(), which is why Postgres 13 is the floor. With seq it forms the (txid, seq) order every reader drains in
resource_typecollection or global
resourceThe collection or global name
operationcreate, update, delete, bulk_update, bulk_delete
record_id, localeNullable, set when the change is specific to one record or locale
payloadjsonb, server-side only. Single writes store shallow scalar { before, after } projections, bulk writes store { count, recordIds }. Nested objects, arrays and hydrated relations are excluded
created_atCapture time, used for delivery-lag observability
settled_atFirst cleanup observation below the PostgreSQL settlement frontier. retentionDays starts here, never at transaction start

Channels, presence, authority fences and topology each own their own table beside it, injected into that same schema.

Next

Pusher and Soketi covers the managed transport, its authorization model, and what revocation can honestly promise.

On this page