Cold-start persist storm blocks hydrates for seconds on the shared SQLite driver (browser/OPFS)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 3.9k
- Forks
- 266
- Avg merge
- 1d 4h
- Merged PRs (30d)
- 55
Description
Related to the persistence hardening RFC #1659.
Setup: 13 Electric collections in one browser tab, all wrapped in persistedCollectionOptions over @tanstack/browser-db-sqlite-persistence 0.2.12 / @tanstack/db-sqlite-persistence-core 0.2.12 (WA-SQLite/OPFS), one database for the whole app. @tanstack/db 0.7.2, @tanstack/electric-db-collection 0.3.18. Measured 2026-08-19 in Chrome via a Playwright journey with adapter-level and driver-level instrumentation.
What we measured on a cold tab (first 30 s):
- 16
applyCommittedTxcalls, 66,126–80,141 mutations, 24.8 s of serialized adapter time, max single op 7,431 ms. - Per-collection snapshot persists: collection A 9,481 mutations → 2,094 ms; collections B/C/D ~14,000 mutations each → 1,647–1,773 ms each.
- Driver FIFO: max queue depth 24, max queue wait 7,199–7,354 ms, max single exec 2,094 ms.
Four things compound into a user-visible 7 s delay applying a live change:
- No statement batching in
applyCommittedTx. Roughly three worker round trips per row (existing-row SELECT, upsert, tombstone delete). 14k mutations take ~1.7–2.1 s where a batched multi-row write would be an order of magnitude cheaper. This is the root cost; everything below is a consequence. - Hydrate windows are not a narrow race. One collection opened 11
loadSubsetcalls per page life, most returning 0–1 rows, and was inside a hydrate window for 52–54% of the tab's first 14 s. Any live transaction arriving in that half is buffered (queuedBecauseHydrating). - A buffered live transaction awaits its own persist. The hydrate it was buffered behind performed a 1-row
loadSubsetthat waited 7,052 / 7,218 / 7,262 ms in the driver FIFO behind the storm;bufferedMsfor the transaction was 6,815 / 6,994 / 7,032 ms. Applying it once flushed took 1–2 ms. 100% of the 7 s was post-network-arrival, inside the persistence layer. - The subset upstream-forward waits for the disk hydrate.
loadSubsetresolves from disk before the upstream request is issued (measured 7,433 ms before forward). In our journey the value arrived on a live stream so this added nothing, but for a change arriving via a subset snapshot it would be the entire delay by itself.
Secondary: getStreamPosition/loadCollectionMetadata for collections created after the storm begins block for the storm's full length, delaying the source sync() start by +8.9 s for two collections.
What we did downstream, and what we could not fix from there. We put a write-behind queue in our persistence adapter: applyCommittedTx resolves on enqueue, nothing starts while a read is pending, and snapshot-sized transactions wait for requestIdleCallback. That took measured propagation from 6.8–7.0 s to 0–1.7 s. The residual is irreducible outside the library: applyCommittedTx is one transaction — rows and stream offset must commit together — so a read arriving mid-write waits out the whole ~2 s op.
Suggestions: batch statements inside applyCommittedTx; chunk large committed transactions into driver-level batches that still commit atomically, yielding between them; and let hydrates be served without queueing behind unrelated collections' writes (per-collection queues, or a read lane).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at persistence core's applyCommittedTx and trace how it interacts with loadSubset, getStreamPosition, and loadCollectionMetadata; use the described Playwright cold-tab journey and driver instrumentation to reproduce queue depth and hydrate delays. Done means large committed transactions avoid per-row round trips, reads are not unnecessarily blocked by unrelated writes, and row changes plus stream offsets retain atomic commit behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- playwright, sqlite, typescript
- Domain
- databases, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100