Skip to content

monarch-orm/monarch-bench

Repository files navigation

monarch-bench

Benchmarks for comparing MongoDB operations across:

  • Monarch ORM
  • Mongoose
  • Native MongoDB driver

The suite uses tinybench and runs against a real MongoDB instance or falls back to an in-memory server via mongodb-memory-server when no MONGO_URI is set.

Requirements

  • Node.js 22+
  • pnpm

Install

pnpm install

Run Benchmarks

pnpm bench

Set MONGO_URI in .env to run against a real MongoDB instance. Without it, an in-memory server is started automatically.

This runs:

  • Initialization benchmarks (initialization, connection)
  • Method benchmarks for schema sizes: small, medium, large
  • Virtuals benchmarks (computed fields via findOne / findMany)
  • Relations benchmarks (findOne, findMany) with and without indexes, across populate styles (populateOne, populateMany, populateBoth) and multi-field variants

Project Structure

  • src/index.ts: benchmark entrypoint and suite orchestration
  • src/setup/: benchmark harness, runner, and schema-sized data builders
  • src/db/: driver/ORM connection and model/schema setup (monarch.ts, mongoose.ts, native.ts)
  • src/bench/init.ts: initialization benchmarks
  • src/bench/methods/: per-method benchmarks (find, updateOne, aggregate, etc.)
  • src/bench/virtuals.ts: virtuals (computed field) benchmarks
  • src/bench/relations/: relation populate benchmarks (findOnePopulate*, findManyPopulate*)

Scripts

  • pnpm bench: run benchmark suite
  • pnpm check: TypeScript check + formatting check
  • pnpm format: check formatting with Prettier
  • pnpm format:fix: apply Prettier formatting

Results

All figures are median throughput (ops/s) — higher is better.

Environment: M2 MacBook Air, MongoDB v7.0.8 (local, Homebrew)


Initialization

Benchmark Monarch Mongoose
initialization 1,362 737
connection 187 146

Monarch bootstraps ~85% faster than Mongoose. Connection setup is ~28% faster. This is a fixed cost per process, so the difference matters most in short-lived or serverless environments.


Methods — Small Schema

Method Monarch Mongoose Native
distinct 12,645 12,164 13,058
find 10,073 7,913 10,603
findById 12,251 11,651 12,533
findByIdAndUpdate 12,397 9,864 12,931
findByIdAndDelete 11,911 8,882 12,201
findOne 12,390 10,174 12,632
findOneAndReplace 10,777 6,021 11,337
findOneAndUpdate 12,579 9,958 13,001
findOneAndDelete 12,429 9,717 17,937
insertOne 16,997 5,587 13,238
insertMany 1,647 695 6,032
bulkWrite 11,199 10,979 11,385
replaceOne 11,511 6,936 11,887
updateOne 12,685 11,696 12,938
updateMany 7,542 7,391 7,884
deleteOne 12,987 11,713 13,086
deleteMany 12,973 11,940 13,108
aggregate 4,348 4,578 4,747
countDocuments 9,934 9,438 10,101
estimatedDocumentCount 16,021 14,414 16,032

Monarch leads on almost every operation and closely tracks native. The biggest gaps are on writes: insertOne is ~3x faster than Mongoose, insertMany ~2.4x. aggregate, bulkWrite, distinct, and updateMany are roughly equal across all three.


Methods — Medium Schema

Method Monarch Mongoose Native
distinct 12,516 12,134 13,230
find 9,581 7,288 9,893
findById 11,934 8,972 12,390
findByIdAndUpdate 12,220 8,807 12,672
findByIdAndDelete 11,511 7,637 11,881
findOne 11,550 8,558 12,320
findOneAndReplace 9,441 4,802 10,017
findOneAndUpdate 12,257 8,801 12,632
findOneAndDelete 11,656 7,824 12,078
insertOne 11,550 4,259 12,552
insertMany 1,099 358 1,504
bulkWrite 11,096 10,934 11,147
replaceOne 10,213 5,600 11,178
updateOne 12,605 11,231 12,889
updateMany 7,762 7,315 7,871
deleteOne 12,931 11,045 13,216
deleteMany 12,841 11,057 12,980
aggregate 877 796 723
countDocuments 10,174 9,494 9,654
estimatedDocumentCount 15,989 14,109 16,260

The write advantage grows with schema size. insertOne is now ~2.7x faster than Mongoose, insertMany ~3x. Read operations maintain a 30–50% lead.


Methods — Large Schema

Method Monarch Mongoose Native
distinct 12,882 11,639 13,333
find 9,167 5,916 9,360
findById 10,830 7,069 11,401
findByIdAndUpdate 11,326 6,369 11,184
findByIdAndDelete 10,811 5,636 11,050
findOne 11,289 7,143 11,348
findOneAndReplace 7,733 2,424 8,259
findOneAndUpdate 11,204 6,014 11,116
findOneAndDelete 10,148 5,643 14,679
insertOne 10,714 2,527 12,952
insertMany 523 222 1,057
bulkWrite 14,193 14,406 14,590
replaceOne 11,486 3,304 12,546
updateOne 16,925 10,652 12,821
updateMany 7,777 6,743 7,466
deleteOne 12,384 8,097 13,205
deleteMany 12,948 9,379 12,308
aggregate 710 711 660
countDocuments 9,852 9,136 9,543
estimatedDocumentCount 15,784 13,552 15,748

At large schema the write gap is most pronounced: insertOne is ~4x faster than Mongoose, insertMany ~2.4x, findOneAndReplace ~3.2x. aggregate and bulkWrite are tied across all three drivers.


Virtuals (computed fields, 100 comments/post)

Benchmark Monarch Mongoose
findOne 12,546 11,310
findMany (n=10) 9,007 4,991
findMany (n=100) 881 481
findMany (n=1000) 168 69

Monarch's virtual computation scales better per document. The gap grows with document count: +11% at findOne, +80% at n=10, +83% at n=100, and +143% at n=1000.


Relations — findOne (no index, 100 comments/post)

Populate style Monarch Mongoose
populateOne 8,236 4,116
populateMany 228 168
populateBoth 247 187
populateOneMulti (3 refs) 4,707 3,147
populateManyMulti (3 FKs) 76 147
populateBothMulti (6 fields) 80 147

populateOne is ~100% faster in Monarch. populateMany and populateBoth (single FK) are ~35% faster. The multi-field many/both cases reverse: Monarch is ~2x slower when resolving 3 foreign-key fields without indexes, as each field requires a full collection scan.


Relations — findOne (indexed, 100 comments/post)

Populate style Monarch Mongoose
populateOne 7,308 4,421
populateMany 3,958 1,477
populateBoth 2,755 1,368
populateOneMulti (3 refs) 4,705 2,885
populateManyMulti (3 FKs) 847 486
populateBothMulti (6 fields) 866 458

Indexes unlock Monarch's populate advantage fully. populateOne is ~65% faster. populateMany and populateBoth are ~2–2.7x faster. Multi-field many/both are ~1.7–1.9x faster.


Relations — findMany (no index, 100 comments/post)

Populate style n Monarch Mongoose
populateOne 10 850 1,419
populateOne 100 105 180
populateMany 10 94 81
populateMany 100 ~1 ~8
populateBoth 10 88 83
populateBoth 100 ~1 ~9
populateOneMulti 10 227 463
populateOneMulti 100 63 105
populateManyMulti 10 31 28
populateManyMulti 100 ~0 ~2
populateBothMulti 10 27 27
populateBothMulti 100 ~0 ~2

Without indexes, Mongoose wins all findMany scenarios. At n=100 with populateMany or populateBoth, Monarch reaches ~780ms per call vs ~117ms in Mongoose. The multi-field variants at n=100 are worse still (~2.75s vs ~402ms). populateOneMulti also shows Mongoose ~2x faster at both scales.

Warning: populateMany and populateBoth at n=100 without indexes reaches ~780ms–2.75s latency per operation in Monarch (vs ~117–405ms in Mongoose). Always use indexed relations when calling findMany with foreign-key populate.


Relations — findMany (indexed, 100 comments/post)

Populate style n Monarch Mongoose
populateOne 10 349 641
populateOne 100 83 179
populateMany 10 168 61
populateMany 100 33 8
populateBoth 10 140 64
populateBoth 100 29 7
populateOneMulti 10 374 534
populateOneMulti 100 62 104
populateManyMulti 10 81 26
populateManyMulti 100 8 2
populateBothMulti 10 75 26
populateBothMulti 100 7 2

With indexes the winner depends on populate direction. For populateOne and populateOneMulti (parent holds the FK), Mongoose is ~2x faster. For populateMany, populateBoth, and their multi variants (child holds the FK), Monarch is ~2–4x faster at n=100.

Note: populateOne and populateOneMulti are actually slower in the indexed collections than in the unindexed ones (for both ORMs). This is because these populate styles resolve posts.pinnedCommentId → comments._id, and _id is always indexed by MongoDB — the extra indexes on the indexed collections provide no benefit for this lookup direction, but do add overhead to the collection.

About

Monarch ORM Benchmarks

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors