Database performance is often reduced to a single throughput number. The performance work in Harper 5.2 takes a broader approach: eliminate repeated storage work on reads, isolate database commits from unrelated application work, make queries use the shortest available path through the data, and keep storage predictable as datasets and table counts grow.
The most sophisticated and innovative change is a new record cache backed by the rocksdb-js Verification Table. Together, these features let Harper safely reuse decoded records across repeated reads without first reading RocksDB to find out whether the cache is still current.
This article takes a technical deep dive into the performance improvements in Harper 5.2. For a higher-level overview of the value these improvements deliver and the use cases they enable, read our Harper 5.2 release announcement.
Making an in-memory record cache safe
A conventional in-memory record cache is easy to make fast, but difficult to make correct (there are [two hard problems](https://martinfowler.com/bliki/TwoHardThings.html), as we say). Harper runs multiple worker threads (often many, sometimes over 30 for larger workloads), each with its own JavaScript heap, while all workers can read and update the same RocksDB databases. A cached record is useful only if a worker can prove that no other worker or transaction has replaced it.
Simply broadcasting cache invalidations between workers would add coordination overhead and still leave difficult races around transactions and snapshots. Reading RocksDB to validate every cached value would be correct, but would eliminate much of the benefit of caching.
The rocksdb-js Verification Table provides a cheaper proof. Despite its name, it is not another RocksDB table. It is a small, fixed-size array of atomic version slots shared by every worker thread in the process. Harper pairs it with a per-worker WeakLRUCache that holds decoded records and their versions.
That cache is more adaptive than a conventional fixed-size LRU. An ordinary LRU makes a binary decision: either it strongly retains an object or it evicts the object and forgets it. Eviction can be wasteful in a JavaScript application because the application may still be using the decoded record. The object remains alive in the heap, but a later lookup can no longer find it through the cache and must read and decode another copy.
WeakLRUCache has two retention stages. A size-weighted least-recently/frequently-used policy strongly retains the hottest records. When a record ages out of that tier, the cache keeps a WeakRef instead of immediately forgetting it. If application code still holds the record, the cache can continue returning that same decoded object at essentially no additional memory cost. Once the last external reference disappears, the garbage collector is free to reclaim it and a FinalizationRegistry removes the dead cache entry.
The result is a cache that follows actual object lifetime. Frequently accessed records stay hot, records actively used by application code remain reusable, and records that are no longer useful disappear through normal garbage collection. Large records are weighted to age out of the strong-retention tier sooner, and the shared expiration policy dynamically gives more of the retention budget to the tables receiving the most traffic.
This is where the combination with the Verification Table becomes especially powerful. Weak references answer, “Is the decoded object still alive?” The Verification Table answers, “Is this exact version still current?” Together they let the application and database share a decoded object for precisely as long as it is both live and valid—without a database read to revalidate it and without retaining it after the runtime wants the memory back. Within a worker, repeated reads can also preserve the same object identity while that record remains alive instead of repeatedly materializing equivalent copies.
That division of responsibility is uniquely efficient: the storage engine supplies a tiny, atomic freshness oracle, while the application runtime manages decoded-object retention using its own reachability information. Neither layer has to pretend it controls the other's memory. The integration gets database-level coherence and garbage-collector-level adaptability at the same time.
On a subsequent read, rocksdb-js hashes the database, column family, and key into a Verification Table slot and compares the slot's version with the cached version:
- Harper finds the decoded record and its version in the worker's local cache.
- rocksdb-js performs a lock-free atomic load from the corresponding Verification Table slot.
- If the versions match, rocksdb-js returns a freshness sentinel without reading RocksDB.
- Harper returns the already-decoded record.
Hash collisions are deliberately safe. Two keys may map to the same slot, but a collision can only cause a conservative cache miss and an unnecessary RocksDB read. It cannot make a stale record appear current. The table is sized such that collisions are rare enough that amortized cost of collisions is quite low.
The first read still loads and decodes the record from RocksDB. As part of that same operation, rocksdb-js conditionally populates the Verification Table with the record's version so later reads can use the fast path.
Coordinating reads with writes
The difficult part is preserving correctness while writes and transactions are in flight.
When a transaction writes a key, rocksdb-js replaces the corresponding version slot with a write-intent marker. Readers encountering that marker fall through to RocksDB rather than trusting a cached value. When the transaction commits or aborts, the slot is settled and old cached versions stop matching across every worker.
Several additional safeguards preserve transaction semantics and provide coordination in the midst of heavy write contention:
- A Verification Table hit still establishes the transaction's RocksDB snapshot, so read-modify-write conflict detection behaves exactly as it would after a full read.
- An older snapshot cannot publish its record version as the newest version in the table.
- Generation-tagged compare-and-swap operations prevent an ABA race in which a write changes a slot and a delayed reader tries to repopulate it with an older version.
- Multiple transactions contending for the same key coordinate through the same write-intent mechanism, allowing retries to resume when the blocking writer finishes instead of polling with arbitrary backoff delays.
The result is a cache that remains local and inexpensive on the JavaScript side, while its freshness signal is shared across every Harper worker, with more efficient contention handling with low-cardinality, high-speed writes.
In Harper's microbenchmarks, warm point reads were approximately 5–8 times faster than uncached reads. Transactional point reads were approximately five times faster. Cold reads remained roughly break-even because they still perform the underlying database read and populate the cache.
The same cache accelerates vector search
Harper 5.2 extends the same mechanism to HNSW vector indexes. HNSW search repeatedly traverses graph nodes stored in RocksDB, and deserializing those nodes can dominate the traversal cost. Harper now stores versioned HNSW nodes so decoded nodes can participate in the Verification Table-backed cache.
In the feature benchmark, this made HNSW searches approximately 2.6 times faster and index builds approximately 2.4 times faster.
Harper 5.2 also adds predicate-aware HNSW traversal. Previously, Harper could find a fixed set of nearest neighbors and apply a filter afterward. With a selective filter, that could return fewer results than requested even when enough matching records existed elsewhere in the graph.
The new traversal evaluates the predicate while exploring the graph and continues until it finds enough matching neighbors or reaches a bounded visit budget. Very selective conditions can switch to an exact scan instead. This improves both result completeness and the amount of work needed for filtered vector queries.
Moving commits off Node's shared worker pool
The read cache removes work. A separate rocksdb-js change prevents database writes from interfering with unrelated work.
Previously, every asynchronous transaction commit occupied one of Node.js's shared libuv worker threads while rocksdb-js wrote the transaction log and committed to RocksDB. Node provides four worker slots by default, shared with asynchronous filesystem operations, crypto, DNS, and RocksDB reads that miss the block cache.
Fast commits normally move through that pool quickly. Under large writes, RocksDB write stalls, or transaction-log contention, however, a burst of commits could occupy every slot. The database was not merely writing more slowly; unrelated operations queued behind those writes also became slow.
rocksdb-js now gives each database a dedicated commit thread. That thread performs the transaction-log write and RocksDB commit in dispatch order, then returns completion to the originating JavaScript environment. This preserves per-database commit ordering while removing commit duration from the shared libuv pool.
In a heavy-write benchmark:
fs.statmedian latency fell from 15.1 milliseconds to 0.12 milliseconds, an improvement of roughly 125 times.fs.statp99 fell from 223.7 milliseconds to 2.6 milliseconds.- Commit throughput increased from 804 to 844 commits per second.
- Small serial commits improved by approximately 15–16%.
The design was also tested against a strained 46 GB database with compaction debt and real write stalls. The dedicated thread improved average commit throughput by 11%, while the worst fs.stat p99 fell from 51 milliseconds to 3.4 milliseconds.
This change matters most when the database is already under stress. It prevents slow storage work from spreading into unrelated parts of the application precisely when predictable latency is most valuable.
A new SQL engine that uses Harper's indexes directly
Harper 5.2 also makes the new Resource API-based SQL engine the default in auto mode. Instead of handing every query to the legacy AlaSQL path, the new engine plans against Harper's own tables and indexes. Unsupported query shapes automatically fall back to the legacy engine.
The benchmark compares the two engines using identical data, alternates requests between them to reduce machine drift, and verifies result parity before counting a timing. At 40,000 rows, all 16 supported query shapes were faster under the new engine, with no result mismatches.
Some representative results were:
The important point is not a fixed multiplier. The legacy engine performed full scans for many of these shapes, so its cost grew with table size. An index-backed primary-key range stayed nearly flat as the table grew from 1,000 to 40,000 rows. The architectural improvement is moving from work proportional to the table to work proportional to the requested index range.
Compression: saves space and faster point reads
Harper 5.2 now enables RocksDB compression, which is enabled by default going forward. The storage.rocks.compression also allows setting selects a codec supported by the installed rocksdb-js build, while the existing storage.compression setting remains the enabled-or-disabled signal when no codec is named. LZ4 is the default compression algorithm.
This also makes upgrades behave consistently: a database created before compression support can use the configured codec for newly written files rather than silently inheriting its old uncompressed setting. Existing SST files are not rewritten during upgrade; they keep their original encoding until normal write traffic replaces them or an operator deliberately performs a rewriting compaction.
Compression applies to both SST blocks and RocksDB blob files. LZ4 is the defaut, but Snappy and Zstandard available for deployments that prefer a different CPU-versus-space tradeoff.
On a one-million-record document workload, the benchmark reported:
Compression did not simply trade CPU for disk space. LZ4 nearly doubled point-read throughput in high-cardinality storage layer benchmarks because compressed blocks put more records into each block-cache slot, outweighing decompression cost. Sequential scans can become slower because they paid the decompression cost without gaining the same cache-density advantage (but generally this isn’t a bottleneck in typical application flow). Workloads can therefore choose based on their access pattern rather than treating compression as only a capacity setting.
One adaptive memory pool for reads and writes
RocksDB normally manages its read cache and write buffers as separate concerns. The block cache retains frequently read SST blocks, while each column family owns memtables for incoming writes. On a database with many tables, independent per-column-family write buffers can multiply the memory ceiling, while a fixed database-wide flush trigger can become too small as those tables divide it among themselves.
Harper instead configures a process-wide WriteBufferManager and connects it to the RocksDB block cache. By default, the block-cache budget is derived from 25% of the memory available to the process, including container memory limits, and the write-buffer allowance is sized to one-third of that budget. Both values remain explicitly configurable through storage.rocks.
The important part is not merely the percentages. Memtable memory is drawn from the block cache, making reads and writes use one coordinated pool. During a write burst, growing memtables displace some cached read blocks. After the memtables flush, that capacity becomes available to the read cache again. A read-heavy workload naturally uses more of the pool for cached blocks; a write-heavy workload can temporarily shift it toward ingestion without creating an independent, additive memory reservation.
The WriteBufferManager is shared across every RocksDB database and column family in the process, so the budget follows total activity rather than multiplying with the number of Harper tables. Its stalling mode applies backpressure at the configured write-memory boundary instead of allowing memtables to grow without limit.
The result is a memory policy that adapts across three dimensions: available host or container memory, the balance of reads and writes, and the number of databases and tables in the process.
Better performance under storage pressure
rocksdb-js also changes the default handling of open RocksDB table files. Previously, RocksDB's unlimited max_open_files default could leave every SST file open. During a sustained bulk ingest with compaction lag, one reproduction reached more than 90,000 open SST descriptors and approached the process limit, after which database writes, sockets, and process spawning could all fail.
The new default derives a bounded file budget from the process's effective file-descriptor limit. Benchmarks found no measurable overhead while the live SST count remained below the budget. If the database exceeds the budget, table-cache eviction can make reads slower, but the process remains alive instead of failing with EMFILE.
This is less visible than a point-read speedup, but it represents the same performance philosophy: healthy operation should remain predictable when the database is under pressure, not just when a benchmark starts from an ideal state.
Faster by doing less—and isolating what remains
The database performance work in Harper 5.2 attacks several different sources of latency:
- The Verification Table and record cache avoid repeated RocksDB reads and record decoding.
- Versioned HNSW nodes apply the same fast path to vector graph traversal.
- Predicate-aware traversal avoids throwing away filtered vector results after the expensive search is already complete.
- The dedicated commit thread prevents slow writes from starving unrelated asynchronous work.
- The new SQL engine replaces full scans with bounded index operations where possible.
- Configurable compression reduces storage and increases effective block-cache density for point reads.
- A shared memory pool dynamically balances block-cache residency with write-buffer demand across every database and table.
- Bounded file-table usage keeps compaction pressure from becoming process-wide failure.
These changes reinforce one another. Repeated reads do less work, writes run in an isolated execution path, and queries reach the right records more directly. The result is not only higher throughput, but more predictable latency across a wider range of real workloads.






.webp)

