Varan Download Varan →
Varan › MongoDB server load

Reading MongoDB without getting your credentials revoked

Varan reads every document to build an exact schema. That is the product — and it was also, measurably, the thing that would have made a DBA switch it off. This is what it cost, what it costs now, and what we gave up to get there.

The metric we had wrong

Every benchmark we had measured how long we waited. A DBA does not care how long you waited. They care how much of their server you consumed, and that number lives in system.profile — the same place they look when something is eating a secondary.

Measured that way, on 192,387 real GitHub events (621 MB, only the _id index, mongod 7.0 / WiredTiger):

Exact schema censusBeforeNow
mongod CPU16,217 ms195 ms
Documents examined5.13× the collection1.01×
Cursor held open (full read)108,668 ms16,160 ms
Fields found950950 — identical

83× less server CPU for a byte-identical census — 0 presence differences, 0 type differences, 0 array-element differences across all 950 paths. The census now costs mongod roughly what a plain find({}) over the same collection costs, which is to say: about what your nightly backup already costs.

Why it was expensive, and what actually fixed it

The old census ran a recursive $objectToArray / $reduce pipeline, so mongod evaluated an expression per field, per document — roughly 1 µs per field. Restructuring the pipeline did not help: $facet, a single-pass tree walk and shape-grouping measured within 1.25× of each other.

What moved it was not making the server faster. It was not asking the server. Varan now reads raw BSON off the wire and walks it itself, so mongod does a plain sequential scan and nothing else.

The trade, stated plainly

64 MB of network to save 16 core-seconds of someone else’s CPU. Inside a VPC that is free; over a thin WAN link it is not, and the server-side engine is one setting away. Bandwidth does not get your credentials revoked. CPU does.

Three other things that were costing the server

The cursor stayed open while we wrote to disk

Hydration read a batch, wrote it into DuckDB, then asked for the next one — so a server-side cursor, and the WiredTiger read snapshot behind it, stayed pinned for the whole load. 108 seconds on a collection that takes 16 seconds to read. Wall-clock never showed it, which is why no benchmark had caught it.

The census read the collection five times

Not by design. Each low-cardinality field triggered its own distinct(), and each of those is a full collection scan. 1,538,126 documents examined for a 300,000-document collection. Those value sets are now collected during the walk that was already happening: 5.13× → 1.01×.

We fetched whole collections to answer filtered questions

A WHERE clause is now translated into a MongoDB $match where — and only where — the translation is provably sound.

PredicateRows fetchedLess wire
type = 'ForkEvent' AND public = 'true'1,89161.3×
type IN (…)9,58914.1×
created_at >= '2024-01-01'28,2796.7×
actor.id > '20000000' — refusedall1.0×

That last row is the interesting one. Varan stores document values as text unless the census proves a field holds exactly one type. Over a text column, '10' sorts before '5', while MongoDB compares numbers — so the two disagree and an ordered predicate must not be pushed. A differential test caught exactly this: an early version pushed it anyway and lost 13,898 of 102,127 matching rows. Fetching too much costs time. Fetching too little is a wrong answer.

Staying correct without asking anyone to reconfigure anything

A cached copy has to know when it is stale. The textbook answer is a change stream, which requires the customer to run a replica set. Varan’s promise is that you install it and it works against what you already have, so that is not an answer.

Instead we count the rows matching the same predicate the copy was loaded under, and compare. Verified on a standalone mongod, no oplog, no extra privileges:

Change at the sourceResult
Row updated into the query’s scope (its key is old)detected
Insert with a non-monotonic key (UUID)detected
Delete inside the scopedetected
In-place edit, membership unchangeddetected (checksum)
Nothing changedcopy reused, 0 bytes read

What a DBA sees

Every operation Varan issues carries a comment. Not “an app is here” — which collection, which run, and which of four partition workers, so a 40-second aggregation can be identified and killed selectively instead of the host being blocked:

varan v=0.7.0 op=census-scan db=analytics coll=events run=a3f9 part=2/4

Reads default to secondaryPreferred, all operations share one concurrency cap per cluster, and there is a stop button.

Caveats

These are on the page for a reason. A benchmark without them is marketing.

What we are not claiming

Not that reading a document store is free. An exact census must see every document, and no amount of engineering removes that. What changed is whose CPU does the work: mongod now does the cheapest thing it knows how to do — a sequential scan — and everything else happens on the machine that asked the question.

Reproduce it

Every figure ships as a script in the repository: benchmark/dba-cost.mjs, census-engine-cost.mjs, spool-cursor-lifetime.mjs, pushdown-differential.mjs, staleness-holes.mjs. The differential benchmarks fail loudly rather than reporting a speed-up, which is how the missing-rows bug above was found.

Try it on your own data →