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.
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";| Option | Default | What it controls |
|---|---|---|
nativeDeltas | false | Keyed row deltas. While off, qualifying topics are served as snapshots |
rowLiveQueries | true | App-wide switch for collection and global row topics |
subscriptionScope | none | Server-owned principal scope folded into scheduler identity. A non-null result over 256 UTF-8 bytes throws |
observer | none | Lifecycle, admission and transport events. Observer failures never break delivery |
admission | see below | Per-session topic, connection and query-shape caps |
connectionAcceptPacingMs | none | Random delay before accepting a new edge session, itself clamped to 30000 ms |
changeBroker | PgNotifyChangeBroker when a pg connection string exists | The cross-instance notice seam |
clientTransport | SSE | The edge delivery seam |
channelEvents | see below | Ordered-channel retention, lease and buffer tuning |
channelPresence | see below | SSE presence leases and convergence |
channelSecurity | see below | Trusted origins, publish rate, authorization deadline |
pollIntervalMs | 15000 with push, 2000 without | Reconciliation interval. 0 gives up that recovery path |
batchSize | 500 | Maximum outbox rows read per drain |
retentionDays | 3 | Minimum time a row remains after it first becomes drainable. 0 disables cleanup |
keepAliveIntervalMs | 8000 | Ping 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.
| Limit | Key | Default |
|---|---|---|
| Topics per connection | maxTopicsPerConnection | 20 |
| Connections per authenticated principal | maxConnectionsPerPrincipal | 5 |
find limit, also applied when omitted | maxFindLimit | 100 |
Nested with depth | maxWithDepth | 3 |
| Concurrent initial snapshots | initialSnapshotConcurrency | 4 |
| Buffered snapshot bytes per session | maxBufferedSnapshotBytes | 1 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.
| Column | Notes |
|---|---|
seq | bigserial primary key, allocated when the row is inserted, not when its transaction commits |
txid | xid8, 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_type | collection or global |
resource | The collection or global name |
operation | create, update, delete, bulk_update, bulk_delete |
record_id, locale | Nullable, set when the change is specific to one record or locale |
payload | jsonb, 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_at | Capture time, used for delivery-lag observability |
settled_at | First 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.
Scaling
A live query that re-runs per subscriber is correct at every size and affordable only at some sizes. This page is the modeling that decides which one you have.
Pusher and Soketi
The managed WebSocket preset, its authorization model, and the honest limits of what terminating a connection can revoke.