npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rumongo

v0.1.1

Published

Rust-native MongoDB read driver for Node.js — faster reads than the official driver and Mongoose

Readme

rumongo

npm version npm downloads license

Built by Piyush Bhangale — 📧 [email protected]. A Rust-native MongoDB read driver that beats the official Node driver and Mongoose on read throughput and event-loop responsiveness.

Rust + Mongo. A Rust-native MongoDB read driver for Node.js (napi-rs). Faster reads than the official Node driver and Mongoose by doing BSON parsing in Rust — pipelined fetch, off-thread parallel parse, and optional lazy zero-copy field access.

Read path only. Writes / hooks / virtuals / populate / validation are not covered — keep the official driver or Mongoose for those. See MIGRATION.md.

Performance

  • 1.6–3.7× faster reads than the official Node driver (by projection width).
  • ~2× vs Mongoose .lean(), ~5× vs hydrated Mongoose.
  • ~10× lower event-loop jitter on a single query; near-zero with lazy or worker-thread offload.
  • 7.3× vs the raw driver when reading a few fields of a wide doc (lazy).

Final benchmark results

Consolidated preset suite (bench/suite.js), 30k docs over a 45-field document, warmup + 6 iters, mean ± sd. Local MongoDB 8.0, Node v20.4, 12 cores. BENCHMARKS.md keeps the full progressive log (every phase); the tables below are the final snapshot.

A) Driver find — official Node driver vs rumongo (eager)

| preset | fields | official (ms) | rumongo (ms) | speedup | |---|---|---|---|---| | few | 4 | 649 ± 95 | 178 ± 17 | 3.65× | | small | 9 | 792 ± 62 | 304 ± 16 | 2.61× | | medium | 15 | 687 ± 49 | 418 ± 54 | 1.64× | | large | 35 | 1532 ± 132 | 841 ± 61 | 1.82× | | full | 45 | 2032 ± 135 | 1031 ± 59 | 1.97× |

B) ODM — Mongoose .lean() vs rumongo Model

| preset | fields | mongoose (ms) | Model (ms) | speedup | |---|---|---|---|---| | few | 4 | 477 ± 53 | 177 ± 18 | 2.69× | | small | 9 | 559 ± 35 | 284 ± 31 | 1.97× | | medium | 15 | 680 ± 68 | 405 ± 35 | 1.68× | | large | 35 | 1455 ± 24 | 850 ± 65 | 1.71× | | full | 45 | 2041 ± 167 | 1031 ± 98 | 1.98× |

C) Event-loop jitter (full preset, single query): official 149.2ms · rumongo 13.8ms (~10× lower).

D) Lazy / wide-doc, few-field read (20k docs × 33 fields, read 2 fields, 10 concurrent): rumongo lazy 7.3× vs the official driver, 2.2× vs rumongo eager.

⚠️ All numbers are localhost (≈0 network latency) — a lower bound for pipeline/concurrency wins, upper bound for CPU-bound wins. The headline 15–20× shows up only under lazy/narrow-read or worker-offload patterns, not eager full-document reads.

How it works (in plain terms)

MongoDB sends every document over the wire as BSON — a compact binary blob. Before your code can use it, something has to decode that binary into objects you can read (doc.name, doc.age, …). The whole speed difference comes down to two questions: who does the decoding (the single JavaScript thread, or many Rust CPU cores) and how much it decodes (every field, or only the ones you touch).

1. A normal find — who decodes the data?

The official driver does all the binary→object work on the one thread that also runs your entire app. rumongo hands that work to Rust, spread across CPU cores, off the main thread — so your app stays responsive.

flowchart TB
  subgraph OFF["🔵 Official Node driver"]
    direction TB
    O1["MongoDB sends BSON (binary)"]
    O2["JS main thread decodes<br/>every field of every doc"]
    O3["Builds thousands of JS objects<br/>(heavy garbage collection)"]
    O4["Your code gets the docs"]
    O1 --> O2 --> O3 --> O4
    OX(["⚠️ All on the ONE thread that<br/>runs your app → event loop frozen"])
    O2 -.-> OX
  end

  subgraph RU["🟢 rumongo (Rust)"]
    direction TB
    R1["MongoDB sends BSON (binary)"]
    R2["Rust fetches batches in a pipeline<br/>(network + parsing overlap)"]
    R3["Many CPU cores decode<br/>batches in parallel"]
    R4["Hand finished result back to JS"]
    R5["Your code gets the docs"]
    R1 --> R2 --> R3 --> R4 --> R5
    RX(["✅ Done in Rust, OFF the JS thread<br/>→ main event loop stays free"])
    R3 -.-> RX
  end

2. findLazy — how much does it decode?

Each document may have 25 fields, but your loop might read only 2. The official driver decodes all of them up front. rumongo keeps the doc as raw bytes and decodes a field only when you touch it.

flowchart LR
  subgraph OFFL["🔵 Official"]
    A1["Get a doc"] --> A2["Decode ALL 25 fields<br/>up front"] --> A3["You read 2 →<br/>23 fields of work wasted"]
  end
  subgraph RUL["🟢 rumongo findLazy"]
    B1["Get a doc,<br/>keep it as raw bytes"] --> B2["Decode a field ONLY<br/>when accessed (doc.name)"] --> B3["You read 2 →<br/>decode 2, skip 23"]
  end

3. Concurrent load — why "jitter" stays low

"Jitter" = how long the event loop froze, i.e. how unresponsive your app got to other users while a query was being decoded. Under many concurrent queries the official driver piles every decode onto the single JS thread; rumongo keeps that work in Rust, off-thread.

sequenceDiagram
  participant App as Your app (event loop)
  participant JS as Official driver
  participant Rust as rumongo (Rust threads)

  Note over App,JS: Official — decode runs ON the event loop
  App->>JS: 10 queries at once
  JS-->>JS: decode all BSON on the JS thread
  Note over App: ⛔ loop frozen — other users wait (high jitter)
  JS-->>App: results

  Note over App,Rust: rumongo — decode runs OFF the event loop
  App->>Rust: 10 queries at once
  Rust-->>Rust: decode BSON on Rust CPU cores
  Note over App: ✅ loop free — app keeps answering (low jitter)
  Rust-->>App: results

One line: the official driver does all the binary→object work on the single thread that also runs your whole app; rumongo pushes that work into Rust on multiple cores, off the main thread, and — in lazy mode — only does the part you actually use.

Install / build

npm install          # deps
npm run build        # compile the Rust addon (release) -> rumongo.<platform>.node
npm run build:ts     # compile the TypeScript layer -> dist/

Requires a Rust toolchain and a reachable MongoDB. Target: linux-x64 (current).

Usage

import { MongoClient } from 'rumongo'

const client = await MongoClient.connect('mongodb://localhost:27017', {
  maxPoolSize: 20,
  serverSelectionTimeoutMs: 5000,
})
const users = client.collection('app', 'users')

// eager: plain JS objects
const all = await users.find({ active: true }, { sort: { age: -1 }, limit: 100 })

await client.close() // IMPORTANT: stops background monitors so the process exits

Three read APIs (pick by shape)

| API | use when | |---|---| | collection.find(filter, opts) | small/medium results; returns plain objects | | collection.findLazy(filter, opts) | wide docs, few fields read — fields parse on access | | collection.findCursor(filter, opts)nextBatch() | large/streaming results; bounded memory |

// lazy: only the fields you touch are parsed
const docs = await users.findLazy({}, { limit: 1000 })
for (const d of docs) console.log(d.name) // parses just `name`

// streaming cursor: process a batch at a time (bounded memory)
const cur = await users.findCursor({}, { batchSize: 1000 })
let batch
while ((batch = await cur.nextBatch()) !== null) {
  for (const d of batch) process(d)
}

Mongoose-style Model (projection pushdown)

import { Model } from 'rumongo'
const User = Model.define(users, { name: 1, age: 1 }) // schema fields = projection
await User.find({ age: { $gte: 18 } }, { sort: { age: 1 } })
await User.findOne({ name: 'Ann' })
await User.findById('507f1f77bcf86cd799439011') // 24-char hex string

Filters with BSON types (Extended JSON)

Filters cross the JS↔Rust boundary as JSON, so use Extended JSON for BSON types:

await users.find({ _id: { $oid: '507f1f77bcf86cd799439011' } })

Worker pool (opt-in) — keep the main loop free under heavy load

Run aggregation/transform work inside worker threads; only the result returns to main, so the event loop stays responsive. Best for "do the work, return a small result" (counts, sums, transforms, exports).

import { WorkerPool } from 'rumongo'
const pool = await WorkerPool.create({ uri, size: 6 })
const { acc } = await pool.reduce('app', 'users', { active: true }, {}, (a, d) => a + d.age, 0)
await pool.close()

Shadow mode — validate before cutover

Run rumongo alongside the official driver, compare, log divergences, but always return the official result.

import { shadow } from 'rumongo'
const res = await shadow(
  () => rumongoColl.find(q),
  () => officialColl.find(q).toArray(),
  (diff) => logger.warn('divergence', diff),
)

Debugging

Set RUMONGO_DEBUG=1 to log each query's collection, doc count, and elapsed time to stderr.

Tests

node --test __tests__/parity/        # 23 parity tests vs official driver
node --test __tests__/model/         # 9 Model tests vs Mongoose
node --test __tests__/lazy/          # lazy field-access tests
node --test __tests__/integration/   # connectivity, pipeline, leak
node bench/suite.js                  # benchmark suite

Install (from npm)

npm install rumongo
const { MongoClient, Model, WorkerPool } = require('rumongo')

Author

Piyush Bhangale

Built rumongo to push MongoDB read performance in Node past the official driver and Mongoose using Rust + napi-rs (off-thread BSON parse, lazy field access, worker-pool offload). Contributions / issues welcome on GitHub.

License

MIT © Piyush Bhangale