Web Development

MongoDB Native Driver vs Mongoose: Performance Benchmarks

December 17th, 2020 | By Camilo Reyes | 5 min read

The time has come to put the MongoDB Native Driver and Mongoose to the test and benchmark how each one performs.

Mongoose is a huge help with MongoDB and offers a bunch of useful features in Node. For performance-sensitive code, is this the best choice?

We will dive into benchmarks via the Apache Benchmark to measure data access strategies. Walk with us in this comparison between the MongoDB Native Driver and Mongoose.

Set-Up

We will use Express to make benchmarks a bit more real since it’s one of the fastest. Only relevant code will be posted, but feel free to check out the entire repo on GitHub.

With the native driver, this POST endpoint creates a new resource:

nativeApp.post('/', async (req, res) => {
  const data = await req.db.native.insertOne({
    number: req.body.number,
    lastUpdated: new Date()
  })
  res.set('Location', '/' + data.ops[0]._id)
  res.status(201).send(data.ops[0])
})


Note that there is a req.db object available that ties into a native database collection:

nativeApp.use((req, res, next) => {
  req.db = {}
  req.db.native= nativeApp.get('db').collection('native')
  next()
})


This use function is middleware in Express. Remember, this intercepts every request and hooks the database to the req object.

For Mongoose, we have similar middleware that does this:

mongooseApp.use((req, res, next) => {
  req.db = {mongoose: mongooseConn.model(
    'Mongoose',
    new Schema({number: Number, lastUpdated: Date}),
    'mongoose')}
  next()
})


Note the use of a Schema that defines individual fields in the collection. If you’re coming from SQL, think of a table as a collection and a column as a field.

The POST endpoint for Mongoose looks like this:

mongooseApp.post('/', async (req, res) => {
  const data = await req.db.mongoose.create({
    number: req.body.number,
    lastUpdated: new Date()
  })
  res.set('Location', '/' + data.id)
  res.status(201).send(data)
})


This endpoint uses the REST-style HTTP status code 201 to respond with the new resource. It is also a good idea to set a Location header with the URL and an id. This makes subsequent requests for this document easier to find.

To eliminate MongoDB from these benchmarks, be sure to set the poolSize to 1 in the connection object. This makes the database-less efficient but puts more pressure on the API itself.

The goal is not to benchmark the database, but the API, and use different strategies in the data layer.

To fire requests to this API, use CURL and a separate port for each strategy:

curl -i -H "Content-Type:application/json" -d "{\"number\":42}" http://localhost:3001/
curl -i -H "Content-Type:application/json" -d "{\"number\":42}" http://localhost:3002/


From this point forward, assume port 3001 has the native driver strategy. Port 3002 is for the Mongoose data access strategy.

Read Performance

The native driver has the following GET endpoint:

nativeApp.get('/:id', async (req, res) => {
  const doc = await req.db.native.findOne({_id: new ObjectId(req.params.id)})
  res.send(doc)
})


For Mongoose, this gets a single document:

mongooseApp.get('/:id', async (req, res) => {
  const doc = await req.db.mongoose.findById(req.params.id).lean()
  res.send(doc)
})


Note that the code in Mongoose is easier to work with. We put lean at the end of the query to make this as efficient as possible. This prevents Mongoose from hydrating the entire object model since it does not need this functionality.

To get a good performance measurement, try benchmarking with and without the lean option in the query.

To fire requests to both endpoints in Apache Benchmark:

ab -n 150 -c 4 -H "Content-Type:application/json" http://localhost:3001/5fa548f96a69652a4c80e70d
ab -n 150 -c 4 -H "Content-Type:application/json" http://localhost:3002/5fa5492d6a69652a4c80e70e


A couple of ab arguments to note: the -n parameter is the number of requests, and the -c parameter is the number of concurrent requests.

On a decent-sized developer box, you will find that it has around 8 logical cores.

Setting the concurrent count to 4 chews up half the cores and frees up resources for the API, database, and other programs. Setting this concurrent count to a high number means it is benchmarking the async scheduler in the CPU, so results might be inconclusive.

Write Performance

For Mongoose, create a PUT endpoint that updates a single document:

mongooseApp.put('/:id', async (req, res) => {
  const { number } = req.body
  const data = await req.db.mongoose.findById(req.params.id)
  data.number = number
  data.lastUpdated = new Date()
  res.send(await data.save())
})


The native driver can do this succinctly:

nativeApp.put('/:id', async (req, res) => {
  const { number } = req.body
  const data = await req.db.native.findOneAndUpdate(
    {_id: new ObjectId(req.params.id)},
    {$set: {number: number}, $currentDate: {lastUpdated: true}},
    {returnOriginal: false})
  res.send(data.value)
})


Mongoose has a similar findOneAndUpdate method that is less expensive but also has fewer features.

When doing benchmarks, it is better to stick to worse-case scenarios. This means including all the features available to make a more informed decision.

Doing a find and then a save in Mongoose comes with change tracking and other desirable features that are not available in the native driver.

To benchmark these endpoints in Apache Benchmark:

ab -n 150 -c 4 -T "application/json" -u .putdata http://localhost:3001/5fa548f96a69652a4c80e70d
ab -n 150 -c 4 -T "application/json" -u .putdata http://localhost:3002/5fa5492d6a69652a4c80e70e


Be sure to create a .putdata file with the following:

{"number":42}


Both endpoints update the lastUpdate field in the document. This is to bust any Mongoose or MongoDB cache that optimizes performance. This forces the database and data access layers to do actual work.

Results and Conclusion

Drumroll, please. Below are the results:

READS

Native

Mongoose

Throughput

1200 #/sec

583 #/sec

Avg Request

0.83 ms

1.71 ms


WRITES

Native

Mongoose

Throughput

1128 #/sec

384 #/sec

Avg Request

0.89 ms

2.60 ms


Overall, the native driver is around 2x faster than Mongoose. Because the native driver uses findOneAndUpdate, read and write results are identical.

The findOneAndUpdate in Mongoose performs identically to findById with the lean option. Mongoose takes a slight ding with Save, but this comes with more features.

Getting rid of the lean-to-prevent hydration does not make a difference because the document object is small.

With these results, one takeaway is to be mindful of performance when choosing to use Mongoose. There is no real reason to exclude the native driver from Mongoose because they are also useful in unison.

For performance-sensitive code, it is best to use the native driver. For feature-rich endpoints that are less performant, it is okay to use Mongoose.

As a final note, subscribe to our newsletter to receive more tutorials about MongoDB, JavaScript, and Application Security.

Jscrambler

The leader in client-side Web security. With Jscrambler, JavaScript applications become self-defensive and capable of detecting and blocking client-side attacks like Magecart.

View All Articles

Must read next

Cybersecurity

The 6 Aspects You Must Secure On Your MongoDB Instances

Authentication, authorization, and accounting are key aspects of MongoDB instances. Here, we explore some quick wins to improve their security.

November 25, 2020 | By Rui Trigo | 7 min read

Web Development

Build Database Relationships with Node.js and MongoDB

Let's dive deeper into MongoDB and understand how relationships work between MongoDB collections in a Node.js server application.

September 10, 2019 | By Connor Lech | 8 min read

Section Divider

Subscribe to Our Newsletter