# Tuning (/docs/infrastructure/realtime/tuning)

---
title: Tuning
description: Every key on RealtimeConfig, the admission limits that reject a topic before it costs anything, and the outbox table the whole runtime reads from.
kind: guide
package: questpie
---

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`

```ts
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.

<Callout type="warn" title="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.
</Callout>

## 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 |

```ts
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](/docs/infrastructure/realtime/pusher)** covers the
managed transport, its authorization model, and what revocation can honestly
promise.
