Batched Replay

Batch cursor, stroke, and movement data into ~4 writes/sec on Convex, then replay each batch with original timing for smooth realtime-looking playback.

Installation

npm install @convex-dev/batched-replay

About Batched Replay

Batched Replay lets senders accumulate high-frequency values (cursors, pen strokes, game positions) and flush them as a single Convex mutation every ~250ms, rather than writing on every frame. Watchers receive each batch and replay it with the original inter-sample timing, so movement looks smooth and continuous at a fraction of the write cost. Batches are persisted, enabling full session replay with configurable retention.

Benefits

Use cases

how to sync cursor positions in realtime with Convex without hitting write limits

The @convex-dev/batched-replay component collects cursor positions locally and flushes them as a single mutation every 250ms using useSender. Watchers use usePlayback to replay each batch with its original inter-sample timing, so movement looks smooth and live while the actual write rate stays around 4 writes per second per sender.

how to build a collaborative drawing board in Convex with stroke replay

Use useSender to batch pointer events into the component's append endpoint, and usePlayback with fromStart: true and an onSample callback to replay every stored stroke in order before continuing live. The onAppend callback lets you mirror completed strokes into your own tables so snapshots can be loaded instead of replaying the full history each time.

how to replay a session or create a timelapse in a Convex app

The useHistory hook loads the full stored batch history for a room and plays it back at a configurable speed multiplier, cutting idle gaps longer than maxGapMs. Batches are retained until retentionMs after they end, or forever if retentionMs is omitted, so complete session replays are available as long as you need them.

how to track multiplayer presence or last seen in Convex

The onAppend callback in @convex-dev/batched-replay fires inside the same transaction as each stored batch, giving you access to the full MutationCtx to write presence, last-seen timestamps, or other derived state into your own tables. The callback receives typed batch data including room, stream, seq, start, end, and samples.

Frequently asked questions

How many Convex writes does batched-replay use per user?

With the default 250ms flush interval, @convex-dev/batched-replay generates about 4 append mutations per second per active sender. Nothing is written while a sender is idle. Read calls scale with senders times viewers times flush rate, so ten people in a room all moving generates roughly 400 function calls per second.

Can I use batched-replay without React?

Yes. The Sender, Player, and HistoryPlayer classes are plain TypeScript and exported directly from @convex-dev/batched-replay. The React hooks useSender, usePlayback, and useHistory are available from @convex-dev/batched-replay/react and wrap these classes.

How do I prevent one user from writing to another user's stream?

The authorizeWrite callback in exposeApi lets you control the stream key the server assigns to each caller. A common pattern is to combine the signed-in user's tokenIdentifier with a per-tab session id, ignoring whatever stream key the client requested. This prevents stream key collisions and unauthorized writes while still supporting multiple tabs per user.

What happens to stored batches over time, and how do I control retention?

Batches are kept in Convex storage until retentionMs milliseconds after their end time, then deleted by a cron job that runs each minute. If retentionMs is omitted, batches are kept indefinitely, which enables full session replay but causes storage to grow with every sample ever sent. Set retentionMs to limit storage growth for high-frequency use cases.

How do I avoid write conflicts when using onAppend across many concurrent senders?

The @convex-dev/batched-replay README explicitly warns against writing to a shared room-level document inside onAppend, because every sender's append would contend on the same document. Instead, write per-stream or per-user documents. For room-level aggregates, the README recommends using @convex-dev/sharded-counter or @convex-dev/aggregate.

Links

Components

Open-source building blocks for your Convex app

Browse components in this category.

Loading components