Click Below to Get the Code

Browse, clone, and build from real-world templates powered by Harper.
Blog
GitHub Logo

Faster by Doing Less: How Harper 5.2 Engineers Database Performance

Harper 5.2 attacks database performance on three fronts: a per-worker record cache validated through lock-free atomic version slots, isolated commit scheduling to keep database writes off the application thread, and query planner improvements that route through the shortest available data path.
Cache
Blog
Cache

Faster by Doing Less: How Harper 5.2 Engineers Database Performance

Kris Zyp
SVP of Engineering
at Harper
August 17, 2026
Kris Zyp
SVP of Engineering
at Harper
August 17, 2026
Kris Zyp
SVP of Engineering
at Harper
August 17, 2026
August 17, 2026
Harper 5.2 attacks database performance on three fronts: a per-worker record cache validated through lock-free atomic version slots, isolated commit scheduling to keep database writes off the application thread, and query planner improvements that route through the shortest available data path.
Kris Zyp
SVP of Engineering

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:

  1. Harper finds the decoded record and its version in the worker's local cache.
  2. rocksdb-js performs a lock-free atomic load from the corresponding Verification Table slot.
  3. If the versions match, rocksdb-js returns a freshness sentinel without reading RocksDB.
  4. 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.stat median latency fell from 15.1 milliseconds to 0.12 milliseconds, an improvement of roughly 125 times.
  • fs.stat p99 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.

Write isolation performance between Harper 5.1 and 5.2. Median latency of unrelated async work falls from 15.1 to 0.12 milliseconds. p99 falls from 223.7 to 2.6 milliseconds. Worst p99 on a strained 46 gigabyte database falls from 51 to 3.4 milliseconds.
Write isolation performance between Harper 5.1 and 5.2
Latency of unrelated async work during heavy writes
5.1, shared pool 5.2, dedicated thread
Median15.1 → 0.12 ms
p99223.7 → 2.6 ms
Worst p99, 46 GB51 → 3.4 ms
0.1 1 10 100 ms

Logarithmic scale. Measured with an unrelated filesystem call competing for the same worker pool as database commits.

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:

SQL query performance between Harper 5.1 and 5.2. Medium inner join falls from 175.819 to 7.301 milliseconds. Indexed ORDER BY with LIMIT falls from 149.570 to 2.755 milliseconds. Ten-row primary-key range falls from 133.599 to 1.648 milliseconds. Small inner join falls from 36.423 to 1.719 milliseconds. Indexed equality over 40 rows falls from 4.094 to 1.446 milliseconds. Primary-key lookup falls from 1.689 to 1.053 milliseconds.
SQL query performance between Harper 5.1 and 5.2
Latency by query shape
5.1, legacy engine 5.2, new engine
Medium inner join175.819 → 7.301 ms
Indexed ORDER BY ... LIMIT149.570 → 2.755 ms
Ten-row primary-key range133.599 → 1.648 ms
Small inner join36.423 → 1.719 ms
Indexed equality, 40 rows4.094 → 1.446 ms
Primary-key lookup1.689 → 1.053 ms
1 3 10 30 100 ms

Logarithmic scale. Identical data, requests alternated between engines, result parity verified before each timing. All 16 supported query shapes were faster; six shown.

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:

Data file size by compression codec, against an 846 megabyte uncompressed baseline. No compression is 846 megabytes, 100 percent. LZ4 is 454 megabytes, 54 percent. Snappy is 434 megabytes, 51 percent. Zstandard is 351 megabytes, 41 percent.
Data file size by compression codec
Each track is the 846 MB uncompressed footprint; fill is what remains on disk
No compression Compressed Space reclaimed
None846 MB100%
LZ4454 MB54%
Snappy434 MB51%
Zstandard351 MB41%
0 200 400 600 800 MB

Linear scale. Percentages are file size relative to no compression, so lower is better. Same dataset written through each codec.

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.

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:

  1. Harper finds the decoded record and its version in the worker's local cache.
  2. rocksdb-js performs a lock-free atomic load from the corresponding Verification Table slot.
  3. If the versions match, rocksdb-js returns a freshness sentinel without reading RocksDB.
  4. 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.stat median latency fell from 15.1 milliseconds to 0.12 milliseconds, an improvement of roughly 125 times.
  • fs.stat p99 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.

Write isolation performance between Harper 5.1 and 5.2. Median latency of unrelated async work falls from 15.1 to 0.12 milliseconds. p99 falls from 223.7 to 2.6 milliseconds. Worst p99 on a strained 46 gigabyte database falls from 51 to 3.4 milliseconds.
Write isolation performance between Harper 5.1 and 5.2
Latency of unrelated async work during heavy writes
5.1, shared pool 5.2, dedicated thread
Median15.1 → 0.12 ms
p99223.7 → 2.6 ms
Worst p99, 46 GB51 → 3.4 ms
0.1 1 10 100 ms

Logarithmic scale. Measured with an unrelated filesystem call competing for the same worker pool as database commits.

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:

SQL query performance between Harper 5.1 and 5.2. Medium inner join falls from 175.819 to 7.301 milliseconds. Indexed ORDER BY with LIMIT falls from 149.570 to 2.755 milliseconds. Ten-row primary-key range falls from 133.599 to 1.648 milliseconds. Small inner join falls from 36.423 to 1.719 milliseconds. Indexed equality over 40 rows falls from 4.094 to 1.446 milliseconds. Primary-key lookup falls from 1.689 to 1.053 milliseconds.
SQL query performance between Harper 5.1 and 5.2
Latency by query shape
5.1, legacy engine 5.2, new engine
Medium inner join175.819 → 7.301 ms
Indexed ORDER BY ... LIMIT149.570 → 2.755 ms
Ten-row primary-key range133.599 → 1.648 ms
Small inner join36.423 → 1.719 ms
Indexed equality, 40 rows4.094 → 1.446 ms
Primary-key lookup1.689 → 1.053 ms
1 3 10 30 100 ms

Logarithmic scale. Identical data, requests alternated between engines, result parity verified before each timing. All 16 supported query shapes were faster; six shown.

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:

Data file size by compression codec, against an 846 megabyte uncompressed baseline. No compression is 846 megabytes, 100 percent. LZ4 is 454 megabytes, 54 percent. Snappy is 434 megabytes, 51 percent. Zstandard is 351 megabytes, 41 percent.
Data file size by compression codec
Each track is the 846 MB uncompressed footprint; fill is what remains on disk
No compression Compressed Space reclaimed
None846 MB100%
LZ4454 MB54%
Snappy434 MB51%
Zstandard351 MB41%
0 200 400 600 800 MB

Linear scale. Percentages are file size relative to no compression, so lower is better. Same dataset written through each codec.

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.

Harper 5.2 attacks database performance on three fronts: a per-worker record cache validated through lock-free atomic version slots, isolated commit scheduling to keep database writes off the application thread, and query planner improvements that route through the shortest available data path.

Download

White arrow pointing right
Harper 5.2 attacks database performance on three fronts: a per-worker record cache validated through lock-free atomic version slots, isolated commit scheduling to keep database writes off the application thread, and query planner improvements that route through the shortest available data path.

Download

White arrow pointing right
Harper 5.2 attacks database performance on three fronts: a per-worker record cache validated through lock-free atomic version slots, isolated commit scheduling to keep database writes off the application thread, and query planner improvements that route through the shortest available data path.

Download

White arrow pointing right

Explore Recent Resources

Blog
GitHub Logo

Faster by Doing Less: How Harper 5.2 Engineers Database Performance

Harper 5.2 attacks database performance on three fronts: a per-worker record cache validated through lock-free atomic version slots, isolated commit scheduling to keep database writes off the application thread, and query planner improvements that route through the shortest available data path.
Cache
Blog
Harper 5.2 attacks database performance on three fronts: a per-worker record cache validated through lock-free atomic version slots, isolated commit scheduling to keep database writes off the application thread, and query planner improvements that route through the shortest available data path.
Person with very short blonde hair wearing a light gray button‑up shirt, standing with arms crossed and smiling outdoors with foliage behind.
Kris Zyp
SVP of Engineering
Blog

Faster by Doing Less: How Harper 5.2 Engineers Database Performance

Harper 5.2 attacks database performance on three fronts: a per-worker record cache validated through lock-free atomic version slots, isolated commit scheduling to keep database writes off the application thread, and query planner improvements that route through the shortest available data path.
Kris Zyp
Aug 2026
Blog

Faster by Doing Less: How Harper 5.2 Engineers Database Performance

Harper 5.2 attacks database performance on three fronts: a per-worker record cache validated through lock-free atomic version slots, isolated commit scheduling to keep database writes off the application thread, and query planner improvements that route through the shortest available data path.
Kris Zyp
Blog

Faster by Doing Less: How Harper 5.2 Engineers Database Performance

Harper 5.2 attacks database performance on three fronts: a per-worker record cache validated through lock-free atomic version slots, isolated commit scheduling to keep database writes off the application thread, and query planner improvements that route through the shortest available data path.
Kris Zyp
News
GitHub Logo

Harper Recognized as an Honorable Mention in the 2026 Gartner® Magic Quadrant™ for Cloud-Native Application Platforms

Harper’s inclusion highlights a broader shift toward integrated cloud-native platforms that unify data, caching, messaging, and application logic. The article explains how Harper’s distributed runtime and managed infrastructure reduce operational complexity, improve performance, and support modern applications and AI agents that require fast, reliable access to state, events, and tools.
Announcement
News
Harper’s inclusion highlights a broader shift toward integrated cloud-native platforms that unify data, caching, messaging, and application logic. The article explains how Harper’s distributed runtime and managed infrastructure reduce operational complexity, improve performance, and support modern applications and AI agents that require fast, reliable access to state, events, and tools.
Person with short dark hair and moustache, wearing a colorful plaid shirt, smiling outdoors in a forested mountain landscape.
Aleks Haugom
Senior Manager of GTM
News

Harper Recognized as an Honorable Mention in the 2026 Gartner® Magic Quadrant™ for Cloud-Native Application Platforms

Harper’s inclusion highlights a broader shift toward integrated cloud-native platforms that unify data, caching, messaging, and application logic. The article explains how Harper’s distributed runtime and managed infrastructure reduce operational complexity, improve performance, and support modern applications and AI agents that require fast, reliable access to state, events, and tools.
Aleks Haugom
Aug 2026
News

Harper Recognized as an Honorable Mention in the 2026 Gartner® Magic Quadrant™ for Cloud-Native Application Platforms

Harper’s inclusion highlights a broader shift toward integrated cloud-native platforms that unify data, caching, messaging, and application logic. The article explains how Harper’s distributed runtime and managed infrastructure reduce operational complexity, improve performance, and support modern applications and AI agents that require fast, reliable access to state, events, and tools.
Aleks Haugom
News

Harper Recognized as an Honorable Mention in the 2026 Gartner® Magic Quadrant™ for Cloud-Native Application Platforms

Harper’s inclusion highlights a broader shift toward integrated cloud-native platforms that unify data, caching, messaging, and application logic. The article explains how Harper’s distributed runtime and managed infrastructure reduce operational complexity, improve performance, and support modern applications and AI agents that require fast, reliable access to state, events, and tools.
Aleks Haugom
News
GitHub Logo

Harper 5.2: More Throughput per Node, Fewer Systems Around It

Harper 5.2 helps architecture and platform leaders improve performance, control infrastructure costs, secure production workloads, and reduce operational complexity by bringing faster data access, agentic capabilities, scheduling, backup, routing, and protection into one unified runtime.
Product Update
News
Harper 5.2 helps architecture and platform leaders improve performance, control infrastructure costs, secure production workloads, and reduce operational complexity by bringing faster data access, agentic capabilities, scheduling, backup, routing, and protection into one unified runtime.
Person with short dark hair and moustache, wearing a colorful plaid shirt, smiling outdoors in a forested mountain landscape.
Aleks Haugom
Senior Manager of GTM
News

Harper 5.2: More Throughput per Node, Fewer Systems Around It

Harper 5.2 helps architecture and platform leaders improve performance, control infrastructure costs, secure production workloads, and reduce operational complexity by bringing faster data access, agentic capabilities, scheduling, backup, routing, and protection into one unified runtime.
Aleks Haugom
Aug 2026
News

Harper 5.2: More Throughput per Node, Fewer Systems Around It

Harper 5.2 helps architecture and platform leaders improve performance, control infrastructure costs, secure production workloads, and reduce operational complexity by bringing faster data access, agentic capabilities, scheduling, backup, routing, and protection into one unified runtime.
Aleks Haugom
News

Harper 5.2: More Throughput per Node, Fewer Systems Around It

Harper 5.2 helps architecture and platform leaders improve performance, control infrastructure costs, secure production workloads, and reduce operational complexity by bringing faster data access, agentic capabilities, scheduling, backup, routing, and protection into one unified runtime.
Aleks Haugom
Blog
GitHub Logo

5 Architectures for Web Personalization

Personalization is a data-delivery problem. Every architectural choice reduces to two distances: compute to user, and compute to fresh data. This piece maps five real architectures against both axes, scored on a concrete retailer workload where stale or slow data breaks the business.
Blog
Personalization is a data-delivery problem. Every architectural choice reduces to two distances: compute to user, and compute to fresh data. This piece maps five real architectures against both axes, scored on a concrete retailer workload where stale or slow data breaks the business.
Person with short dark hair and moustache, wearing a colorful plaid shirt, smiling outdoors in a forested mountain landscape.
Aleks Haugom
Senior Manager of GTM
Blog

5 Architectures for Web Personalization

Personalization is a data-delivery problem. Every architectural choice reduces to two distances: compute to user, and compute to fresh data. This piece maps five real architectures against both axes, scored on a concrete retailer workload where stale or slow data breaks the business.
Aleks Haugom
Jul 2026
Blog

5 Architectures for Web Personalization

Personalization is a data-delivery problem. Every architectural choice reduces to two distances: compute to user, and compute to fresh data. This piece maps five real architectures against both axes, scored on a concrete retailer workload where stale or slow data breaks the business.
Aleks Haugom
Blog

5 Architectures for Web Personalization

Personalization is a data-delivery problem. Every architectural choice reduces to two distances: compute to user, and compute to fresh data. This piece maps five real architectures against both axes, scored on a concrete retailer workload where stale or slow data breaks the business.
Aleks Haugom