# Row deltas (/docs/client/realtime/deltas)

---
title: Row deltas
description: A snapshot resends the whole page on every change. A delta sends the one row that moved. Most apps never need to ask for it, and this page says why.
kind: guide
package: questpie
---

Snapshot is the default and it is the safe one. The server re-runs your query
and pushes the whole result. Correctness never depends on a client applying a
patch in the right order.

Deltas save bandwidth on a big, busy list. That is the whole benefit, and they
stay off until two separate switches are on.

## Check what you already have

`streamEvents()` on a collection `find` topic already gives you keyed
`insert`, `update` and `delete` frames. The client diffs consecutive snapshots
and emits the difference, keyed by `String(row.id)`.

So a store that applies row events works today, with no server change. What
server-side deltas buy you is the wire cost, not the shape.
[Raw subscriptions](/docs/client/realtime/raw-api) covers those frames.

Turn to this page when snapshots themselves are the problem. A hundred rows
resent every second is the case that justifies it.

## Turn it on

Step one is the server. `nativeDeltas` is `false` by default.

```ts title="src/questpie/server/questpie.config.ts"
import { runtimeConfig } from "questpie/app";

import env from "./env";

export default runtimeConfig({
	db: { url: env.DATABASE_URL },
	realtime: { nativeDeltas: true },
});
```

Deploy that in a second pass, not the first. Writers append to the outbox without
a lock, and readers order it by `(txid, seq)` under PostgreSQL's visibility
watermark. A node still running an older reader orders by sequence alone, which
can skip a row. Ship the code, wait for the fleet, then flip the flag.

Step two is the topic. Deltas are opt-in per subscription, and only through the
raw API.

```ts
for await (const event of client.realtime.streamEvents<PostsPage>({
	resourceType: "collection",
	resource: "posts",
	operation: "find",
	mode: "delta",
	where: { published: true },
})) {
	apply(event);
}
```

`live()` and `liveIter()` have no `mode`. They stay on snapshots on purpose.

<Callout type="info" title="Asking early is safe">
	With `nativeDeltas` off, a `mode: "delta"` topic gets the ordinary snapshot
	stream. Nothing errors and nothing is lost. So you can write the consumer
	first and turn the server flag on later.
</Callout>

## Which queries qualify

The shape rules are strict, and a topic that breaks any of them silently stays
on snapshots.

| Rule                              | Why                                            |
| --------------------------------- | ---------------------------------------------- |
| A collection, not a global        | A global is a single row                       |
| `operation: "find"`               | `count` and `get` have no row set to key       |
| No `limit`, `offset` or `orderBy` | A window makes membership depend on other rows |
| No `with`                         | Relations are not tracked in delta mode        |
| `columns` must keep `id`          | `id` is the delta key                          |
| No `RAW` in `where`               | The server cannot inspect raw SQL              |
| No relation field in `where`      | Membership would depend on another table       |

Pusher and Soketi are snapshot-only, whatever the topic asks for.

## What arrives

The first frame is always a full snapshot. Keyed events follow, then an
`up-to-date` frame closes each batch and carries the new `totalDocs`.

Reduce each event as it lands, but do not render until `up-to-date` arrives. One
transaction can produce several row events, and the states in between are not
results anyone should see. `subscribe()` and `stream()` already hold the value
back for you. Do the same in a hand-written consumer.

A reset is a fresh snapshot with `reset: true`. Replace your state with it
rather than merging. Resets happen when:

- A minute passes. Every delta group re-bootstraps on a timer
- Something the query depends on changes, rather than the collection itself
- One transaction touches more rows than the delta budget allows
- The server's queue for this group overflows

Access is re-checked on every bootstrap and on every row hydration. It runs the
same row, field and `afterRead` path a normal read uses. A delta never skips a
rule that a `find()` would apply.

## The budget

Delta topics have their own caps, separate from the 100-row snapshot limit.

| Cap                      | Default | What it bounds               |
| ------------------------ | ------: | ---------------------------- |
| `maxDeltaFindLimit`      |     384 | Rows a delta topic may track |
| `maxBufferedDeltaEvents` |     512 | Events queued for one group  |
| `maxBufferedDeltaBytes`  |   1 MiB | Bytes queued for one group   |

`maxDeltaFindLimit` is lowered further when the byte caps imply a smaller
bootstrap. Go past the row cap and the topic is refused with
`reason: "query_limit"`. A bootstrap too large to serialize is refused with
`reason: "snapshot_bytes"`.
[Errors and limits](/docs/client/realtime/errors) covers both.

## Next

**[Scalable realtime](/docs/infrastructure/realtime/scaling)** is the wider
question deltas are one answer to: what to do when one change has to reach a
lot of people.
