Skip to content

Performance

Peerborne makes several deliberate performance tradeoffs to balance security, consistency, and resource use. This page documents the design decisions, benchmark suites, and tuning guidance.

On a quorum-bound full load, Peerborne strips inline changes, enumerates the served change-tree CIDs, and drains at most eight concurrent Helia blockstore.get() calls before invoking sync(). Successful gets may populate the local blockstore; the gate precedes CRDT/ACL mutation, not every local storage write. The same eight-worker cap also bounds the hash-only block prefetch of tracked bootstrap and invitation catch-up loads (including quorum-disabled loads) and the missing-block fetch pool during sync; only the exhaustive prefetch after inline changes are stripped is quorum-specific.

Source: peerborne-document.ts — LOAD_PREFETCH_MAX_CONCURRENCY and the bounded worker pool for blockstore fetches.

When enabled (the default), document open probes at most loadQuorumK distinct currently connected peer IDs. Effective K is min(loadQuorumK, deduplicatedConnectedPeerCount). With K ≥ 2, the default Q is floor(K / 2) + 1; an explicit Q is a hard floor and is never reduced. Effective K = 1 is rejected unless loadQuorumAllowSinglePeer is explicitly enabled.

Q distinct authenticated writer authorities must agree on a digest covering the locally trusted security tuple, document, served frontier, and complete response manifest. Peerborne binds the selected response to that digest and the fresh request challenge before mutation. Many transport PeerIds sharing one credential count once; independently controlled trusted authorities are still an application requirement:

ParameterDefaultRationale
loadQuorumK3Caps the connected peers probed; fewer connected peers reduce effective K
loadQuorumQMajority of effective Kfloor(K / 2) + 1; an explicit value is never lowered
loadQuorumTimeoutMs5000Per-peer timeout; peer probes run in parallel
loadQuorumAllowSinglePeerfalseRequires an explicit opt-in before effective K = 1 can pass

Source: peerborne-config.ts. Focused tests cover pure decisions and orchestration with mocked probes; they are not a hostile real-network quorum test.

Both CRDT adapters use parallel deserialization for cold-cache performance to reduce time-to-interactive when opening large documents:

Focused BeeKEM tests show that a removed reader’s leaf (blanked in the tree) cannot derive the root produced by the corresponding PathUpdate. This avoids the primitive-level flaw of encrypting a successor key under a predecessor key already known to the removed reader. It is not an end-to-end revocation guarantee: PathUpdate delivery is best-effort, BeeKEM state is memory-only, and multi-peer partition/rejoin revocation is not verified. Source: peerborne-document.ts and wire-protocols.ts. The removed-member cases in beekem-revocation.test.ts and the encoded-delivery cases in beekem-revocation-wire.test.ts provide the focused evidence for that primitive property.

  • LRU cache: Used for document change blocks and key material. Limits unbounded memory growth. Verified by lru-cache.test.ts.
  • Compaction: Automatic snapshot triggering is disabled by default. A writer can still call snapshot() manually when the CRDT provider supports snapshots. With pruneAfterSnapshot, snapshot creation prunes the in-memory served sync tree while retaining recent document nodes and preserving ACL nodes. Helia block deletion remains off by default (gcAfterPrune: false); when enabled, eligible pruned-block deletion is scheduled asynchronously. Focused tests cover configuration and pruning/GC helper decisions; long-running concurrent multi-peer compaction is not exercised.
  • React hook caches: Module-level task and subscriber-count caches with ref-counting eviction. Verified by hooks-cache.test.ts and hooks-lifecycle.test.ts.

V2 memory indexes use sorted compound-key arrays with binary-search lookup; incremental insert/delete can still shift O(N) entries. IndexedDB uses compound secondary indexes and bounded cursors. Equality/range lookup visits the selected range, but first is not yet an early-termination guarantee and count: 'exact' exhausts all matches. Execution metadata reports rows visited and whether sorting stayed in index order. See the indexing design.

Peerborne includes six benchmark suites across two packages. Each suite uses a statistical runner that reports min, max, mean, median, p99, and standard deviation with warmup iterations and optional memory-delta tracking.

The charts below are generated from the committed benchmark-snapshot.json snapshot. It was collected on Apple M5 hardware with 32 GB RAM, Node.js 22.19.0, and macOS arm64. Most points use 20 timed iterations; the convergence suite uses four and the blind-index field series uses ten because those suites intentionally reduce the requested count for their more expensive cases.

Treat the values as a directional, single-machine snapshot—not a performance guarantee or a comparison with other systems. The convergence chart is a local simulation, not a browser, relay, or wide-area network measurement. Dots show means and capped whiskers show p99. No chart establishes a pass/fail budget.

Source: packages/core/src/__benchmarks__/

SuiteFileWhat it measures
CRDT Sync Latencycrdt-sync-latency.tsPer-operation latency of the change pipeline at payload sizes 1 KB to 1 MB: ECDSA P-384 sign/verify, AES-GCM encrypt/decrypt, combined sign+encrypt and decrypt+verify pipelines, JSON serialize/deserialize
Crypto Overheadcrypto-overhead.tsKey generation cost (ECDSA P-384, AES-GCM-256), key rotation at 10 KB, plaintext vs encrypted change propagation across sizes, isolated crypto operation overhead
Convergence Simulationconvergence-simulation.tsSimulated multi-peer convergence: 2–32 peers making concurrent edits through the full sign-encrypt-broadcast-decrypt-verify pipeline, measures time-to-convergence, message count, and bandwidth
Line chart of mean sign-and-encrypt and decrypt-and-verify latency from 1 KB to 1 MB, with p99 whiskers.
Mean signed-and-encrypted change latency by payload size; whiskers show p99.
View chart at full sizeFit chart to page
Line chart comparing mean plaintext and encrypted synthetic pipeline latency from 1 KB to 1 MB, with p99 whiskers.
Mean plaintext and encrypted pipeline latency by payload size; whiskers show p99.
View chart at full sizeFit chart to page
Line chart of local simulated convergence latency from two to 32 peers on a logarithmic latency axis, with p99 whiskers.
Local simulated convergence latency by peer count on a logarithmic axis; whiskers show p99.
View chart at full sizeFit chart to page

Source: packages/index/src/__benchmarks__/

SuiteFileWhat it measures
Index Query Scalingindex-query-scaling.tsQuery latency vs index size: 100–100K documents. Exact-match, range, prefix, compound, and sorted queries against MemoryIndexStorage, plus single-document update cost and full-scan baseline
Bloom Filter Scalingbloom-filter-scaling.tsInsert throughput at filter sizes 1K–1M bits, positive/negative query time, false-positive rate at fill counts 100–10K, serialization/deserialization time, CRDT merge (join) time, memory footprint
Blind Index Performanceblind-index-perf.tsEncrypted search operations: field-key derivation (HKDF), single/compound token generation, token match/mismatch comparison, field-count scaling (1–16 fields), batch throughput (100 tokens)
Line chart comparing mean exact-match, prefix, and compound local index query latency from 100 to 10,000 documents, with p99 whiskers.
Mean local index-query latency by document count; whiskers show p99.
View chart at full sizeFit chart to page
Line chart comparing the mean cost to insert 1,000 items and merge filters across Bloom filter sizes from 1 Kb to 1 Mb, with p99 whiskers.
Mean Bloom-filter insertion and merge latency by filter size; whiskers show p99.
View chart at full sizeFit chart to page
Line chart of mean blind-index key derivation and tokenization latency from one to 16 fields, with p99 whiskers.
Mean blind-index key derivation and tokenization latency by field count; whiskers show p99.
View chart at full sizeFit chart to page

Several test suites exercise performance-critical paths:

Test suitePathWhat it covers
Load quorumload-quorum.test.tsQuorum decision logic, hash-binding comparison, config validation
Load quorum orchestratorload-quorum-orchestrator.test.tsMulti-peer quorum scenarios including timeout and insufficient-response paths
LRU cachelru-cache.test.tsEviction correctness, capacity bounds, size tracking
Blockstore GCblockstore-gc.test.tsCID collection, deletable-filtering, tree-shape coverage
Compactioncompaction.test.tsConfig defaults, GC decision logic, prune+GC integration
Network statisticsnetwork-stats.test.tsMessage/byte counters, connection tracking, snapshot isolation
React hook cacheshooks-cache.test.ts and hooks-lifecycle.test.tsCache population, ref-counted eviction, per-document isolation
Cross-NAT throughputnat-resilience.spec.ts10-message bidirectional burst across NAT boundaries with delivery threshold
  • ~1 MB threshold: Consider splitting documents when encoded state exceeds ~1 MB. Decoding and encoding cost degrades with very large documents.
  • Browser limits: Tens of MB may cause performance issues in browsers.
  • Tombstone accumulation: Operations that frequently delete and re-add items in Y.Array create permanent tombstones (~40–80 bytes each) that grow document size monotonically. Batch mutations in Yjs transactions to reduce overhead.
  • Nesting depth: Keep nested types to 2–3 levels.

See the Yjs schema design cookbook for detailed guidance on document structure and size management.

  • Local CRDT mutations become visible before the outbound storage/publication pipeline completes, so the local UI does not wait for a network round trip.
  • No end-to-end update-latency budget is enforced, and relay-backed live post-load browser propagation is not yet an acceptance test.
  • WebTransport and direct DCUtR/WebRTC paths are configured but do not yet have transport-specific Peerborne synchronization evidence in CI; the current cross-NAT document proof uses Circuit Relay for initial history load.

When operating a Circuit Relay server under load:

  • Increase ulimit -n for many concurrent connections.
  • Set NODE_OPTIONS=--max-old-space-size=4096 for memory headroom.
  • Monitor event loop lag.
  • For large deployments, tune GossipSub parameters (D, Dlo, Dhi, heartbeatInterval) in relay-server/src/index.ts.
  • Client-side: circuitRelayTransport({ reservationConcurrency: 1 }) — increase for redundancy at the cost of additional relay connections.

See the running a relay cookbook for setup instructions.

NetworkStats provides counters for messages/bytes sent and received, document open/close lifecycle events, and connection tracking. Applications can use snapshot() to inspect current state. Verified by network-stats.test.ts.

  • No pass/fail performance budgets. Benchmarks exist but have no thresholds in CI. A regression that doubles latency would not be caught automatically.
  • Bundle size. The core barrel eagerly imports the complete networking/storage stack. Example application bundles are approximately 2.0 MB minified.
  • No automated bottleneck detection. There is no profiling or flame-graph generation in CI.
Terminal window
# Core benchmarks
yarn workspace @peerborne/core benchmark --iterations 100
# Index benchmarks; bound the largest query dataset for a smoke run
yarn workspace @peerborne/index benchmark --iterations 100
yarn workspace @peerborne/index benchmark --iterations 1 --max-documents 100

Both suites output Markdown tables with statistical summaries. The --iterations flag controls the sample count (default 100); --max-documents bounds index-query scaling only.

To replace the committed measurement snapshot and regenerate its SVG charts, run both suites with the recorded parameters, then collect and render:

Terminal window
yarn workspace @peerborne/core benchmark --iterations 20
yarn workspace @peerborne/index benchmark --iterations 20 --max-documents 10000
yarn workspace @peerborne/site benchmarks:snapshot --core-iterations 20 --index-iterations 20 --max-documents 10000
yarn workspace @peerborne/site charts:generate
yarn workspace @peerborne/site charts:test

Benchmark collection is intentionally separate from SVG rendering. Rerunning a benchmark produces new measurements; rendering the same committed JSON snapshot must produce byte-identical charts, which the site build checks.