Database NotesRohit’s interview study guide
Chapter 23

MongoDB Operations and Security

Running MongoDB safely: backups with mongodump and mongorestore, users and SCRAM, role-based access control, TLS, client-side and Queryable Encryption, and the profiler, all shown on real servers.

6 topics
Parts marked Advanced are extra depth. Skip them on a first read or a quick revision.

#Backup and restore

A backup is only as good as the restore you've practised. MongoDB gives you two families. Logical backups with mongodump export the documents as BSON files plus each collection's metadata (options and index definitions), and mongorestore loads them back, rebuilding the indexes. Physical backups copy the data files themselves: a filesystem or cloud-volume snapshot, or the managed backups in Atlas and Ops Manager. From my notes: "Backup & restore: mongodump and mongorestore". That's the right starting point, and the one interviewers expect you to know by name.

The mental model: mongodump is photocopying every page of a book; a snapshot is photographing the whole shelf at once. Photocopying is portable and selective (one database, one collection, even a query), but slow for a big library, and the pages you copy first may change before you finish. The snapshot is fast and consistent, but all-or-nothing and tied to the same storage format.

This ran on the three-member rs22 replica set from the previous chapter, against the ch22 database (customers, products, orders, plus an index on orders.customerId): dump the database, have someone drop a collection by mistake, and restore just that collection.

Shell
mongodump --db=ch22 --out=/backup/2026-10-16
cat /backup/2026-10-16/ch22/orders.metadata.json
mongosh --quiet ch22 --eval 'db.orders.drop()'                  # the accident
mongorestore --nsInclude="ch22.orders" /backup/2026-10-16
mongosh --quiet ch22 --eval 'db.orders.countDocuments()'
mongosh --quiet ch22 --eval 'db.orders.getIndexes().map(i => i.name)'
Result
2026-10-16T16:16:37.701+0000	writing `ch22.products` to `/backup/2026-10-16/ch22/products.bson`
2026-10-16T16:16:37.702+0000	writing `ch22.customers` to `/backup/2026-10-16/ch22/customers.bson`
2026-10-16T16:16:37.702+0000	writing `ch22.orders` to `/backup/2026-10-16/ch22/orders.bson`
2026-10-16T16:16:37.702+0000	done dumping `ch22.orders` (3 documents)
2026-10-16T16:16:37.702+0000	done dumping `ch22.customers` (6 documents)
2026-10-16T16:16:37.702+0000	done dumping `ch22.products` (8 documents)
{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"customerId":{"$numberInt":"1"}},"name":"customerId_1"}],"uuid":"eadf70171b7948f880cebe3c4278b7a0","collectionName":"orders","type":"collection"}
true
2026-10-16T16:16:37.893+0000	preparing collections to restore from
2026-10-16T16:16:37.893+0000	don't know what to do with file `/backup/2026-10-16/ch22/prelude.json`, skipping...
2026-10-16T16:16:37.893+0000	reading metadata for `ch22.orders` from `/backup/2026-10-16/ch22/orders.metadata.json`
2026-10-16T16:16:37.903+0000	restoring `ch22.orders` from `/backup/2026-10-16/ch22/orders.bson`
2026-10-16T16:16:37.913+0000	finished restoring `ch22.orders` (3 documents, 0 failures)
2026-10-16T16:16:37.913+0000	restoring indexes for collection `ch22.orders` from metadata
2026-10-16T16:16:37.913+0000	index: &idx.IndexDocument{Options:bson.M{"name":"customerId_1", "v":2}, Key:bson.D{bson.E{Key:"customerId", Value:1}}, PartialFilterExpression:(*bson.D)(nil)}
2026-10-16T16:16:37.949+0000	3 document(s) restored successfully. 0 document(s) failed to restore.
3
[ '_id_', 'customerId_1' ]
What’s happening
  1. mongodump --db=ch22 connects like any client (to localhost:27017 by default), reads every collection in ch22 and writes one .bson file of documents plus one .metadata.json per collection into /backup/2026-10-16/ch22/. It dumps up to 4 collections in parallel, which is why the log lines interleave.
  2. orders.metadata.json holds the collection's options and the index definitions, _id_ and customerId_1, not the index data: indexes are rebuilt on restore. A prelude.json next to it records the server and tools versions ({"ServerVersion":"9.0.4","ToolVersion":"100.19.1"}).
  3. db.orders.drop() is the accident (true): the collection and its indexes are gone.
  4. mongorestore --nsInclude="ch22.orders" picks one namespace out of the dump directory. It recreates the collection, inserts the 3 documents, then builds customerId_1 from the metadata. The "don't know what to do with prelude.json" line is harmless.
  5. Afterwards the collection is back with both indexes. The dump is portable: the same directory could be restored into another server, another database name or Atlas, as long as it runs the same major version or feature compatibility version.
AdvancedRestoring over existing data or renaming

mongorestore only inserts. Restoring into a collection that still has the documents fails per document and carries on:

Shell
mongorestore --nsInclude="ch22.orders" /backup/2026-10-16            # orders still exist
mongorestore --nsInclude="ch22.orders" --drop /backup/2026-10-16     # drop first, then restore
mongorestore --nsFrom="ch22.*" --nsTo="ch22_restore.*" /backup/2026-10-16
Result
2026-10-16T15:54:31.940+0000	continuing through error: E11000 duplicate key error collection: ch22.orders index: _id_ dup key: { _id: 1 }
2026-10-16T15:54:31.940+0000	continuing through error: E11000 duplicate key error collection: ch22.orders index: _id_ dup key: { _id: 2 }
...
2026-10-16T15:54:24.139+0000	0 document(s) restored successfully. 3 document(s) failed to restore.
2026-10-16T15:54:24.227+0000	3 document(s) restored successfully. 0 document(s) failed to restore.
2026-10-16T15:54:24.326+0000	17 document(s) restored successfully. 0 document(s) failed to restore.
What’s happening
  1. The first restore hits E11000 for every document, because each _id already exists. It never overwrites: 0 document(s) restored successfully. 3 document(s) failed.
  2. --drop drops each target collection before restoring it, so the result is exactly the dump. That's what you want for "put it back as it was", and it's also how a careless restore wipes newer data.
  3. --nsFrom/--nsTo rename namespaces on the way in: all 17 documents land in a new ch22_restore database, the safe way to inspect a backup next to live data before copying anything back.
AdvancedConsistent dumps with --oplog

A plain dump copies collections one after another, so if writes continue, collections are copied at different moments. On a replica set member, --oplog also captures the oplog entries written during the dump, and --oplogReplay replays them on restore, so the result is one consistent moment: the end of the dump. Here a script keeps inserting orders every 10 ms during a full dump of a 400,000-document database:

Shell
mongodump --oplog --out=/backup/full
mongorestore --host=solo:27017 --oplogReplay /backup/full     # into a fresh, empty mongod
mongosh --host=solo:27017 --quiet ch22 --eval 'db.orders.countDocuments()'
Result
2026-10-16T15:55:25.251+0000	writing `ch22.big` to `/backup/full/ch22/big.bson`
2026-10-16T15:55:25.251+0000	writing `ch22.orders` to `/backup/full/ch22/orders.bson`
...
2026-10-16T15:55:25.252+0000	done dumping `ch22.orders` (123 documents)
2026-10-16T15:55:25.454+0000	done dumping `ch22.big` (400000 documents)
2026-10-16T15:55:25.454+0000	writing captured oplog to ``
2026-10-16T15:55:25.454+0000		dumped 15 oplog entries
...
2026-10-16T15:55:47.289+0000	finished restoring `ch22.orders` (123 documents, 0 failures)
2026-10-16T15:55:48.636+0000	replaying oplog
2026-10-16T15:55:48.637+0000	applied 15 oplog entries
2026-10-16T15:55:48.650+0000	400137 document(s) restored successfully. 0 document(s) failed to restore.
137
What’s happening
  1. orders was copied early, when it held 123 documents. The big collection took another 200 ms, during which the writer kept inserting.
  2. Meanwhile --oplog recorded every oplog entry from the start of the dump to its end: 15 entries, saved as oplog.bson at the top of the dump directory.
  3. The restore loads the 123 copied orders, then replays the 15 entries. The oplog is idempotent (previous chapter), so an entry for a document the copy already contained does no harm.
  4. The restored orders has 137 documents: the state at the moment the dump finished, not a mix of moments. 400,137 documents in total: 400,000 in big, 137 orders and the other two collections.
  5. Limits: --oplog needs a full dump (with --db it fails: Failed: bad option: --oplog mode only supported on full dumps), it doesn't work through mongos, and it fails if someone renames a collection or changes users during the dump.
AdvancedCompressed single-file archives

--archive writes the whole dump as one file (or to stdout, to pipe somewhere) and --gzip compresses it:

Shell
mongodump --db=ch22 --archive=/backup/ch22.archive.gz --gzip
ls -l /backup/ch22.archive.gz
Result
2026-10-16T15:54:24.342+0000	done dumping `ch22.customers` (6 documents)
-rw-r--r-- 1 root root 1070 Oct 16 15:54 /backup/ch22.archive.gz
AdvancedRestoring from an archive

Restoring it (here into a new database name) reads the same single file:

Shell
mongorestore --archive=/backup/ch22.archive.gz --gzip --nsFrom="ch22.*" --nsTo="ch22_archive.*"
Result
2026-10-16T16:15:53.065+0000	17 document(s) restored successfully. 0 document(s) failed to restore.
AdvancedWhat mongodump can't do

Things to know:

  • mongoexport/mongoimport are not backups. They write JSON or CSV, which can lose BSON types (dates, Decimal128, ObjectId need Extended JSON) and carry no index definitions. Use them for data exchange.
  • Sharded clusters: mongodump through mongos can't use --oplog, so writes during the dump make it inconsistent across shards. Use coordinated snapshots, Atlas or Ops Manager, or follow the documented procedure of stopping the balancer and dumping each shard.
  • Encrypted collections: the docs say mongodump isn't supported for Queryable Encryption or CSFLE collections, and it doesn't decrypt anything. Here it did dump them, as ciphertext (see Field-level and Queryable Encryption); export decrypted data with a driver instead.
  • Time: the dump itself is fast, but a restore rebuilds every index. On a large database that can take hours, which is why big systems restore from snapshots.
  • Run mongodump against a secondary (--readPreference=secondary) to keep the load off the primary. The default is the primary.

#Authentication: SCRAM and users

Authentication is proving who you are; authorisation (next topic) is what you're then allowed to do. A fresh mongod has access control off: anyone who can reach the port can read and drop everything. Turn it on with --auth (or security.authorization: enabled in the config file), and every client must log in as a user.

Users live in a database, their authentication database, and are identified as user@db. From my notes: "Creating a user after switching to the admin DB". That's the convention for administrators and most app users: create them in admin and grant roles on other databases. The default mechanism is SCRAM (Salted Challenge Response Authentication Mechanism): the password is never sent, not even hashed. The client and server each prove they know a key derived from the password and a per-user salt, so someone sniffing the connection or stealing system.users doesn't get the password.

The mental model: a club with a doorman. With --auth off there's no doorman. With it on, the doorman has a list (the users), and for each name a lock that only the right key opens; the list never contains the keys themselves.

This ran on a separate mongo:9.0 container started with --auth and no users yet:

mongosh
// connected without credentials, from inside the server's own machine (localhost)
db.products.find()
use admin
db.createUser({
  user: "rohit",
  pwd: "change-me-123",           // in real use: pwd: passwordPrompt()
  roles: [
    { role: "userAdminAnyDatabase", db: "admin" },
    { role: "readWriteAnyDatabase", db: "admin" }
  ]
})
db.getSiblingDB("ch22").products.insertOne({ _id: 1, name: "ThinkBook 14" })
Result
Uncaught MongoServerError[Unauthorized]: not authorized on ch22 to execute command { find: "products", filter: {}, lsid: { id: UUID("ab9b04fa-da31-4be0-b4ff-378ea7b24ae7") }, $db: "ch22" }
switched to db admin
{ ok: 1 }
Uncaught MongoServerError[Unauthorized]: Command insert requires authentication
What’s happening
  1. With --auth on, the anonymous find is rejected: not authorized on ch22. The connection itself was allowed; it just has no privileges.
  2. There are no users yet, so how do you create the first one? The localhost exception: while no user exists, a client connecting over localhost may create the first user in the admin database. That's why this shell runs on the server's own machine.
  3. use admin then createUser makes rohit@admin with two built-in roles: userAdminAnyDatabase (manage users everywhere) and readWriteAnyDatabase (read and write every non-system database). The server stores salted SCRAM keys, not the password.
  4. The moment that user exists, the localhost exception closes. The same unauthenticated shell can no longer do anything: Command insert requires authentication.
  5. From now on every client logs in. That's the whole bootstrap: start with --auth, create an admin over localhost, then use that admin to create everyone else.
AdvancedPitfall: the authentication database

The same user, three connection strings:

Shell
mongosh "mongodb://rohit:wrong-password@localhost/ch22?authSource=admin"
mongosh "mongodb://rohit:change-me-123@localhost/ch22"
mongosh "mongodb://rohit:change-me-123@localhost/ch22?authSource=admin"
Result
MongoServerError: Authentication failed.
MongoServerError: Authentication failed.
AdvancedServer log: telling failures apart

The server log tells the two failures apart:

Output
"msg":"Failed to authenticate","attr":{..."mechanism":"SCRAM-SHA-256","user":"rohit","db":"admin","error":"AuthenticationFailed: SCRAM authentication failed, storedKey mismatch"...}
"msg":"Failed to authenticate","attr":{..."mechanism":"SCRAM-SHA-256","user":"rohit","db":"ch22","error":"UserNotFound: Could not find user \"rohit\" for db \"ch22\""...}
What’s happening
  1. Wrong password: the client's proof doesn't match the stored key, storedKey mismatch.
  2. Right password, no authSource: the driver uses the database in the URI path, ch22, as the authentication database. There's no rohit@ch22, so UserNotFound.
  3. The client sees the same Authentication failed. for both on purpose, so an attacker can't probe which user names exist. When a correct password "doesn't work", check authSource first.
  4. The third connection, with authSource=admin, logs in and runs normally (the next example uses it). The path database (ch22) is then just the default database for commands.
AdvancedWhat the server stores for SCRAM

Logged in as rohit, look at the connection and at what the server keeps for the user:

mongosh
db.runCommand({ connectionStatus: 1 }).authInfo
db.getSiblingDB("admin").getUser("rohit").mechanisms
db.getSiblingDB("admin").system.users.findOne({ user: "rohit" }, { _id: 1, credentials: 1 })
Result
{
  authenticatedUsers: [ { user: 'rohit', db: 'admin' } ],
  authenticatedUserRoles: [
    { role: 'readWriteAnyDatabase', db: 'admin' },
    { role: 'userAdminAnyDatabase', db: 'admin' }
  ]
}
[ 'SCRAM-SHA-1', 'SCRAM-SHA-256' ]
{
  _id: 'admin.rohit',
  credentials: {
    'SCRAM-SHA-1': {
      iterationCount: 10000,
      salt: 'q52UKCqrFxzNDsDpC7NNGA==',
      storedKey: 'OXal846+UWguItpbjfXMREqKfS4=',
      serverKey: 'H6KDkrHZvutq8Ue3siFTAk7rVw4='
    },
    'SCRAM-SHA-256': {
      iterationCount: 15000,
      salt: 'qUdPR9LmNjcn2prxWpziXDayy+VCfKxziYMf4A==',
      storedKey: 'NpCzfynXRfXvIq5cpj/E1bY69/A4NxcUl2VZWDEhy6o=',
      serverKey: '/1LNqSTFqPqWM608VoKf5mA6jBaSimJE64cNM5HQe4A='
    }
  }
}
What’s happening
  1. connectionStatus shows who this connection is and which roles it holds right now, useful when a permission error doesn't make sense.
  2. The user was created with both mechanisms. Drivers negotiate and use SCRAM-SHA-256 (the log above shows "mechanism":"SCRAM-SHA-256"); SCRAM-SHA-1 is kept for old clients. MongoDB 4.0 added SCRAM-SHA-256 and removed the older MONGODB-CR mechanism.
  3. For each mechanism the server keeps a random salt, an iteration count (10,000 and 15,000 rounds of hashing, which makes guessing passwords slow) and two derived keys, never the password.
  4. During login the server sends the salt and a nonce; the client derives the keys from the password and proves it has them; then the server proves it holds serverKey, so the client knows it isn't talking to an impostor. That mutual proof is what "challenge-response" means.
AdvancedPractical rules for users
  • One user per application and per person, with only the roles it needs (next topic). Never let apps use the admin user.
  • pwd: passwordPrompt() in mongosh keeps passwords out of shell history; in apps, read credentials from a secret store or environment, and URL-encode special characters in connection strings.
  • The official Docker image creates a root user and enables auth when you set MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD. Without them, the container runs with access control off.
  • Internal authentication: replica set and shard members authenticate to each other with security.keyFile (a shared secret) or x.509 certificates. Setting a keyfile also turns on client access control.
  • Change passwords with db.changeUserPassword(), list users with db.getUsers(), remove with db.dropUser().
  • Exposing an unauthenticated mongod to the internet is the classic MongoDB breach: since 3.6 mongod binds only to localhost by default for that reason. --bind_ip_all (used in these containers) is safe only on a private network.

#Role-based access control

Once users log in, role-based access control (RBAC) decides what each one may do. A privilege is a set of actions (find, insert, update, remove, createIndex, dropCollection…) on a resource (a database, one collection, or the cluster). A role is a named bundle of privileges and other roles. A user gets roles, and can do exactly what the union of its roles allows, nothing more. From my notes: "Authentication & authorization, RBAC".

The mental model is office key cards. Nobody gets a master key; the receptionist's card opens the lobby, the accountant's opens the lobby and finance, and the cards are defined once by job (roles), not per person.

MongoDB ships built-in roles for the common jobs:

RoleAllows
readfind, aggregate, list collections on one database
readWriteread plus insert, update, delete, create/drop collections and indexes on one database
dbAdminindexes, stats, profiler, validation on one database (no data reads)
userAdmincreate users and roles on one database
dbOwnerreadWrite + dbAdmin + userAdmin on one database
clusterMonitorread-only monitoring (serverStatus, rs.status(), $currentOp)
backup / restorewhat mongodump / mongorestore need
readAnyDatabase, readWriteAnyDatabase, userAdminAnyDatabase, dbAdminAnyDatabasethe same, on every database (granted in admin)
rooteverything

Two users on the same ch22 database, created by rohit: a reporting user that can only read, and the shop's app user that can read and write:

mongosh
use ch22
db.createUser({ user: "reporter", pwd: "reporter-pw", roles: [{ role: "read", db: "ch22" }] })
db.createUser({ user: "shopApp", pwd: "shopapp-pw", roles: [{ role: "readWrite", db: "ch22" }] })

// logged in as reporter@ch22
db.orders.countDocuments()
db.orders.updateOne({ _id: 1 }, { $set: { status: "cancelled" } })
db.products.drop()

// logged in as shopApp@ch22
db.orders.updateOne({ _id: 1 }, { $set: { status: "cancelled" } }).modifiedCount
db.getSiblingDB("admin").system.users.find()
db.createUser({ user: "sneaky", pwd: "x", roles: ["dbOwner"] })
Result
switched to db ch22
{ ok: 1 }
{ ok: 1 }
3
Uncaught MongoServerError[Unauthorized]: not authorized on ch22 to execute command { update: "orders", updates: [ { q: { _id: 1 }, u: { $set: { status: "cancelled" } } } ], ordered: true, lsid: { id: UUID("783eaa4e-902d-4c66-b2d3-553796bf3d5f") }, $db: "ch22" }
Uncaught MongoServerError[Unauthorized]: not authorized on ch22 to execute command { drop: "products", lsid: { id: UUID("783eaa4e-902d-4c66-b2d3-553796bf3d5f") }, $db: "ch22" }
1
Uncaught MongoServerError[Unauthorized]: not authorized on admin to execute command { find: "system.users", filter: {}, lsid: { id: UUID("542b49dc-600e-4f8b-bfb5-d97f3c00bf5f") }, $db: "admin" }
Uncaught MongoServerError[Unauthorized]: not authorized on ch22 to execute command { createUser: "sneaky", pwd: "xxx", roles: [ "dbOwner" ], lsid: { id: UUID("4ee02aac-dfde-46fb-8488-8d74b715e889") }, $db: "ch22" }
What’s happening
  1. Both users are created in ch22 this time, so their authentication database is ch22 (they log in with authSource=ch22, the default when the URI path is ch22). Either convention works; be consistent.
  2. reporter has read on ch22: the count works (3), but the update and the drop fail with Unauthorized: not authorized on ch22 to execute command { update … } and { drop … }. The error names the exact command that was refused.
  3. shopApp has readWrite: the update works (modifiedCount 1).
  4. readWrite on ch22 gives nothing elsewhere: reading admin.system.users is refused, and so is creating users, because user management is a separate privilege (userAdmin). Note the server masks the password in the echoed command (pwd: "xxx").
  5. This is least privilege: each identity gets only what its job needs, so a leaked reporting password can read data but never change or delete it.
AdvancedCustom role with a view for fields

A support agent should update order status and look up a customer's name and city, but never see email addresses or touch products. No built-in role fits, so a custom role grants find and update on orders only, plus find on a view that projects away everything but name and city:

mongosh
use ch22
db.createView("customerDirectory", "customers", [{ $project: { _id: 0, name: 1, city: 1 } }])
db.createRole({
  role: "supportAgent",
  privileges: [
    { resource: { db: "ch22", collection: "orders" }, actions: ["find", "update"] },
    { resource: { db: "ch22", collection: "customerDirectory" }, actions: ["find"] }
  ],
  roles: []
})
db.createUser({ user: "asha", pwd: "asha-pw", roles: ["supportAgent"] })

// logged in as asha@ch22
db.orders.updateOne({ _id: 3 }, { $set: { status: "cancelled" } }).modifiedCount
db.orders.insertOne({ _id: 9, customerId: 2, status: "paid" })
db.customerDirectory.find().limit(2)
db.customers.findOne()
db.runCommand({ connectionStatus: 1, showPrivileges: true }).authInfo.authenticatedUserPrivileges
  .map(p => `${p.resource.collection}: ${p.actions.join(", ")}`)
Result
{ ok: 1 }
{ ok: 1 }
{ ok: 1 }
1
Uncaught MongoServerError[Unauthorized]: not authorized on ch22 to execute command { insert: "orders", documents: [ { _id: 9, customerId: 2, status: "paid" } ], ordered: true, lsid: { id: UUID("4ee8076b-ec10-41cb-be27-f53f07689dc6") }, $db: "ch22" }
[
  { name: 'Rohit', city: 'Toronto' },
  { name: 'Maya', city: 'Mumbai' }
]
Uncaught MongoServerError[Unauthorized]: not authorized on ch22 to execute command { find: "customers", filter: {}, limit: 1, lsid: { id: UUID("4ee8076b-ec10-41cb-be27-f53f07689dc6") }, $db: "ch22" }
[ 'orders: find, update', 'customerDirectory: find' ]
What’s happening
  1. createView defines customerDirectory as a stored aggregation over customers that outputs only name and city. A view holds no data; it runs the pipeline on each read.
  2. createRole lists exact privileges: find and update on ch22.orders, find on ch22.customerDirectory. The role lives in ch22, so it can only grant privileges on ch22.
  3. asha can update an order (1) but not insert one: insert isn't in the action list.
  4. She can read the view, which shows names and cities only, but reading customers directly is refused. That's field-level access control in MongoDB: there's no column GRANT as in SQL, so you grant on a view instead.
  5. connectionStatus with showPrivileges: true lists the effective privileges, the quickest way to debug "why can't this user do X".
AdvancedPitfall: userAdmin is as strong as root
AdvancedManaging roles, and how SQL compares
  • db.grantRolesToUser() / db.revokeRolesFromUser() change a user's roles; db.grantPrivilegesToRole() changes a custom role. Changes apply to open connections too.
  • db.getRoles({ showPrivileges: true, showBuiltinRoles: true }) documents what every role really allows.
  • Roles created in admin can grant privileges on any database and on the cluster resource; roles in other databases only on their own database.
  • Compared with MySQL (see Users, roles and GRANT): MySQL grants at database, table and column level and has GRANT … ON db.table TO user; MongoDB grants at database and collection level, and uses views for field-level restriction. Neither has row-level security built in (PostgreSQL does); in MongoDB you approximate it with a view that filters with $match, or in the app.
  • Auditing (recording who did what) is an Enterprise and Atlas feature.

#TLS

Authentication protects the door; TLS protects the corridor. Without it, everything between the app and mongod crosses the network in plain text: queries, documents, and the SCRAM exchange (which doesn't reveal the password, but everything after it is readable). From my notes: "TLS/SSL". TLS encrypts the connection and lets the client verify it's really talking to your server; with client certificates, the server can verify the client too, which is mutual TLS.

The mental model: TLS is an armoured van. Anyone can see the van drive past, but not what's inside, and the van checks the guard's ID at the bank (the server certificate) before handing anything over. Mutual TLS means the bank also checks the van's papers.

For this demo, a throwaway certificate authority (CA) signed a server certificate for the host name tlsnode and a client certificate for a "reporting-job". The server was started with TLS required and the CA configured, which also makes it require client certificates:

Shell
mongod --auth --bind_ip_all --tlsMode requireTLS \
       --tlsCertificateKeyFile /tls/server.pem --tlsCAFile /tls/ca.pem

mongosh "mongodb://tlsnode/"                                                    # 1. no TLS
mongosh "mongodb://tlsnode/?tls=true&tlsCAFile=/tls/ca.pem"                     # 2. TLS, no client certificate
mongosh "mongodb://tlsnode/?tls=true&tlsCAFile=/tls/other-ca.pem&tlsCertificateKeyFile=/tls/client.pem"   # 3. wrong CA
mongosh "mongodb://tlsnode/?tls=true&tlsCAFile=/tls/ca.pem&tlsCertificateKeyFile=/tls/client.pem"         # 4. all correct
Result
MongoServerSelectionError: connection <monitor> to 192.168.117.6:27017 closed
MongoServerSelectionError: connection <monitor> to 192.168.117.6:27017 closed
MongoNetworkError: self-signed certificate in certificate chain
1
What’s happening
  1. A plain connection to a requireTLS server is closed straight away. The client only sees "connection closed"; the server logs The server is configured to only allow SSL connections.
  2. With tls=true and the right CA, the client trusts the server's certificate, but the server has --tlsCAFile and expects a client certificate too. None was sent: no SSL certificate provided by peer; connection rejected.
  3. With a client certificate but the wrong CA file, the client refuses the server: it can't chain the server's certificate to a CA it trusts, self-signed certificate in certificate chain. That check is what stops a man-in-the-middle.
  4. With the right CA and a client certificate signed by it, the handshake succeeds and db.hello().ok prints 1. (The user still has to authenticate before reading anything, because --auth is on.)
  5. If you only want encryption and server verification, add --tlsAllowConnectionsWithoutCertificates on the server; then case 2 succeeds and client certificates become optional.
AdvancedServer log for rejected connections

The server log for the first two attempts:

Output
"msg":"Error receiving request from client. Ending connection from remote","attr":{"error":{"code":141,"codeName":"SSLHandshakeFailed","errmsg":"The server is configured to only allow SSL connections"}
"msg":"Error receiving request from client. Ending connection from remote","attr":{"error":{"code":141,"codeName":"SSLHandshakeFailed","errmsg":"no SSL certificate provided by peer; connection rejected"}
AdvancedAuthenticating with x.509 certificates

A client certificate can replace the password entirely: the user is named after the certificate's subject and lives in the special $external database:

mongosh
// as an admin
db.getSiblingDB("$external").runCommand({
  createUser: "CN=reporting-job,OU=reporting,O=ch22 Apps",
  roles: [{ role: "read", db: "ch22" }]
}).ok

// mongosh "mongodb://tlsnode/ch22?tls=true&tlsCAFile=/tls/ca.pem&tlsCertificateKeyFile=/tls/client.pem&authMechanism=MONGODB-X509"
db.runCommand({ connectionStatus: 1 }).authInfo.authenticatedUsers
db.products.find().toArray()
db.products.insertOne({ _id: 1 })
Result
1
[
  { user: 'CN=reporting-job,OU=reporting,O=ch22 Apps', db: '$external' }
]
[ { _id: 1, name: 'ThinkBook 14' }, { _id: 2, name: 'MacBook Air' } ]
Uncaught MongoServerError[Unauthorized]: not authorized on ch22 to execute command { insert: "products", documents: [ { _id: 1 } ], ordered: true, lsid: { id: UUID("2d609802-e5b9-48dc-afe7-83dd59d33376") }, $db: "ch22" }
What’s happening
  1. The user name is the certificate subject in RFC 2253 order, exactly as openssl x509 -noout -subject -nameopt RFC2253 prints it. $external means "the credential is checked outside MongoDB", here by the TLS handshake.
  2. authMechanism=MONGODB-X509 with no user name or password: the server takes the identity from the verified client certificate, CN=reporting-job,…@$external.
  3. RBAC applies as usual: the role is read on ch22, so the find works and the insert is refused.
  4. The first attempt failed with Cannot create an x.509 user with a subjectname that would be recognized as an internal cluster member, because the client certificate had the same organisation (O=ch22) as the server's own certificate, and MongoDB treats matching O/OU/DC as "another cluster member". Client certificates need a different organisation or unit from member certificates.
AdvancedInspecting the server's TLS settings

The running server's settings and which TLS versions clients actually used:

mongosh
db.adminCommand({ getCmdLineOpts: 1 }).parsed.net
db.serverStatus().transportSecurity
Result
{
  bindIp: '*',
  tls: {
    CAFile: '/tls/ca.pem',
    certificateKeyFile: '/tls/server.pem',
    mode: 'requireTLS'
  }
}
{
  '1.0': Long('0'),
  '1.1': Long('0'),
  '1.2': Long('0'),
  '1.3': Long('101'),
  unknown: Long('0')
}
AdvancedTLS modes, versions and host names
  • net.tls.mode is one of disabled, allowTLS, preferTLS, requireTLS. The middle two exist to roll TLS out across a running cluster without downtime: allowTLS → preferTLS → requireTLS.
  • The net.ssl.* options and --ssl* flags are the pre-4.2 names; they still appear in old configs and tools (the database tools still say --ssl).
  • Every connection here used TLS 1.3. TLS 1.0 has been disabled by default since 4.0 where newer versions are available; net.tls.disabledProtocols controls it.
  • The host name in the connection string must be in the server certificate's subject alternative names. Connecting to the server's IP address instead of tlsnode failed with Hostname/IP does not match certificate's altnames: IP: 192.168.117.6 is not in the cert's list.
  • tlsAllowInvalidCertificates and tlsAllowInvalidHostnames switch those checks off. Fine for a quick local test, never in production: they bring back the man-in-the-middle risk.
  • TLS protects data in transit only. Encryption at rest is the Enterprise encrypted storage engine, Atlas (always encrypted), or encryption of the disk or volume.

#Field-level and Queryable Encryption

TLS protects data on the wire and disk encryption protects it at rest, but in both cases the server sees plain text: a DBA, a stolen backup or a compromised server can read every field. In-use encryption goes further: the client encrypts sensitive fields before they leave the app, so the server only ever stores and handles ciphertext, and only clients holding the keys can read them. From my notes: "Field-level encryption". MongoDB has two flavours:

  • Client-Side Field Level Encryption (CSFLE), since 4.2. Fields are encrypted either deterministically (same plaintext → same ciphertext, so equality queries work) or randomly (stronger, not queryable).
  • Queryable Encryption (QE), since 7.0. Always randomised, yet the server can still answer equality and range queries (8.0+), and since 9.0 prefix, suffix and substring queries on strings, without decrypting anything. MongoDB recommends it for new apps.

Both use envelope encryption: each field is encrypted with a data encryption key (DEK); the DEKs are stored in a key vault collection, themselves encrypted by a customer master key (CMK) that lives in a KMS (AWS KMS, Azure Key Vault, Google Cloud KMS, a KMIP server, or a local key for testing). The mental model is a safe-deposit box: the bank (the server) stores your sealed box and can find it by its number, but only you hold the key.

Each also comes in two modes. Automatic encryption reads a schema and encrypts the right fields for you, but needs a query-analysis library and is available only in Enterprise and Atlas. Explicit encryption, where your code calls encrypt(), works on Community too, and that's what runs here, in mongosh on the three-member rs22 replica set (in-use encryption doesn't work on a standalone):

mongosh
// A 96-byte master key. In production this lives in a KMS, never in code.
const masterKey = require("crypto").randomBytes(96);
const encClient = Mongo("mongodb://m1:27017,m2:27017,m3:27017/?replicaSet=rs22", {
  keyVaultNamespace: "encryption.__keyVault",
  kmsProviders: { local: { key: masterKey } },
  bypassAutoEncryption: true          // explicit encryption: we call encrypt() ourselves
});
const keyId = encClient.getKeyVault().createKey("local", { keyAltNames: ["kyc-key"] });
const ce = encClient.getClientEncryption();
const DET = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic";
const RND = "AEAD_AES_256_CBC_HMAC_SHA_512-Random";
const kyc = encClient.getDB("ch22").kyc;
kyc.drop();
kyc.insertOne({ customerId: 2, name: "Maya",
                passportNo: ce.encrypt(keyId, "Z1234567", DET),
                notes: ce.encrypt(keyId, "Verified by video call", RND) })

db.getSiblingDB("ch22").kyc.findOne({}, { _id: 0 })      // a normal connection: what the server stores
kyc.findOne({}, { _id: 0 })                              // the key-holding client: decrypted automatically
kyc.findOne({ passportNo: ce.encrypt(keyId, "Z1234567", DET) }, { _id: 0, name: 1 })
Result
true
{
  acknowledged: true,
  insertedId: ObjectId('6ad24a2af296439ae083b4e5')
}
{
  customerId: 2,
  name: 'Maya',
  passportNo: Binary.createFromBase64('AW3RCd58V0PJoepBxLLujHQC4uJbFZZMgamFpxj/f8mAHCMPDaq8FSq8oScqTTU7yNlpGuvNfE/qvt+TAVtPq6LXhLJLqdR2DKiREzKW4WTbdQ==', 6),
  notes: Binary.createFromBase64('Am3RCd58V0PJoepBxLLujHQCuMQSeunJgL1KVMmln8G3CjPLgMPTW9T2dco7rdOAgcu4tueMBWyzUB/7HGPNzqg+fDolGJFg+r29IsbhtM3IRsVSffb+lwuMvJiT6MSNa50=', 6)
}
{
  customerId: 2,
  name: 'Maya',
  passportNo: 'Z1234567',
  notes: 'Verified by video call'
}
{ name: 'Maya' }
What’s happening
  1. Mongo(uri, { keyVaultNamespace, kmsProviders }) opens a second connection with encryption support. createKey("local") generates a data key, encrypts it with the master key and stores it in encryption.__keyVault. The first true in the output is kyc.drop().
  2. ce.encrypt(keyId, value, algorithm) runs in the client: the passport number is encrypted deterministically, the notes randomly. The insert sends only ciphertext to the server.
  3. Through a normal connection, the server's view: both fields are Binary subtype 6, MongoDB's type for encrypted values. Anyone reading the database, the oplog or a backup sees exactly this.
  4. Through the key-holding client, the same findOne comes back decrypted. Automatic decryption works on Community too: the client sees subtype 6, fetches the data key from the vault, decrypts it with the master key and decrypts the field.
  5. The equality query works because deterministic encryption produced identical ciphertext: the client encrypts "Z1234567" the same way and the server compares two blobs without ever seeing the passport number.
AdvancedDeterministic vs random encryption

Encrypt the same passport number twice with each algorithm, and try to query the random field:

mongosh
ce.encrypt(keyId, "Z1234567", DET).toString("base64") === ce.encrypt(keyId, "Z1234567", DET).toString("base64")
ce.encrypt(keyId, "Z1234567", RND).toString("base64") === ce.encrypt(keyId, "Z1234567", RND).toString("base64")
kyc.findOne({ notes: ce.encrypt(keyId, "Verified by video call", RND) })
Result
true
false
null
What’s happening
  1. Deterministic encryption of the same value twice gives identical ciphertext. That's what makes { passportNo: <ciphertext> } match.
  2. Random encryption gives different ciphertext every time, so the same query on notes finds nothing (null). Random fields can be stored and read back but never searched.
  3. The price of deterministic: anyone looking at the database can see that two customers have the same passport number, or count how common a value is. For a field with few values (a yes/no flag, a blood type) that leaks nearly everything, so CSFLE's advice is deterministic only for high-cardinality values you must query, random for the rest. Queryable Encryption removes this trade-off.
AdvancedQueryable Encryption on Community 9.0

QE needs an encrypted collection, created with the list of encrypted fields and the queries each supports. Explicit QE works without the Enterprise library: bypassQueryAnalysis: true and encrypt() with the Indexed algorithm.

mongosh
const masterKey = require("crypto").randomBytes(96);
const encClient = Mongo("mongodb://m1:27017,m2:27017,m3:27017/?replicaSet=rs22", {
  keyVaultNamespace: "encryption.__keyVault", kmsProviders: { local: { key: masterKey } }, bypassQueryAnalysis: true });
const ce = encClient.getClientEncryption();
const { encryptedFields } = ce.createEncryptedCollection("ch22", "patients", {
  provider: "local",
  createCollectionOptions: { encryptedFields: { fields: [
    { path: "ssn", bsonType: "string", keyId: null, queries: { queryType: "equality" } } ] } }
});
const keyId = encryptedFields.fields[0].keyId;
const patients = encClient.getDB("ch22").patients;
patients.insertOne({ name: "Liam", ssn: ce.encrypt(keyId, "901-23-4567", { algorithm: "Indexed", contentionFactor: 8 }) })
patients.insertOne({ name: "Zara", ssn: ce.encrypt(keyId, "555-11-2222", { algorithm: "Indexed", contentionFactor: 8 }) })
patients.find({ ssn: ce.encrypt(keyId, "901-23-4567", { algorithm: "Indexed", queryType: "equality", contentionFactor: 8 }) },
              { _id: 0, name: 1, ssn: 1 }).toArray()
db.getSiblingDB("ch22").patients.findOne({ name: "Liam" }, { _id: 0 })
db.getSiblingDB("ch22").getCollectionNames().filter(n => n.includes("patients"))
db.getSiblingDB("ch22").patients.insertOne({ name: "Noah", ssn: "123-45-6789" })
Result
{
  acknowledged: true,
  insertedId: ObjectId('6ad24a3a763fd975b0dde595')
}
{
  acknowledged: true,
  insertedId: ObjectId('6ad24a3a763fd975b0dde596')
}
[ { name: 'Liam', ssn: '901-23-4567' } ]
{
  name: 'Liam',
  ssn: Binary.createFromBase64('DtkH5c7P5U5Vluc3LVnVi6oCv81htbFmVD946RI/jA3NnjzU2uqPSTS7yJVneoI2nx6mJPoNBWIsAJkGyorMe3t+kn2snUIagAGXwURKXQmKD1ALsngCoReYlk0Tiemp5Rgmh5hGcJh7lNylKUTgY31weU4FZGoGnNseOiTU0nvBCVh7UfEWtcvGWryMEiAgasbSj5YMl6WyZLuHE2yG4hZ+lf7xNrU8zJCWWXWOuaaXanRXKUhaOGE4/0Q41FtdZplr3ibrCINSKE0XuIC5hAlghBmNHeAcOgnvCs9My3U5bg==', 6),
  __safeContent__: [
    Binary.createFromBase64('lf7xNrU8zJCWWXWOuaaXanRXKUhaOGE4/0Q41FtdZpk=', 0)
  ]
}
[ 'enxcol_.patients.ecoc', 'patients', 'enxcol_.patients.esc' ]
Uncaught:
MongoServerError: Document failed validation
Additional information: {
  failingDocumentId: ObjectId('6ad24a43e6127a3b51bbdefb'),
  details: {
    operatorName: 'implicitFLESchema',
    schemaRulesNotSatisfied: [
      {
        operatorName: 'properties',
        propertiesNotSatisfied: [
          {
            propertyName: 'ssn',
            details: [
              {
                operatorName: 'fle2Encrypt',
                reason: 'value was not encrypted'
              }
            ]
          }
        ]
      }
    ]
  }
}
What’s happening
  1. createEncryptedCollection creates a data key for ssn (keyId: null means "make one") and the collection with its encryptedFields definition.
  2. Each insert encrypts the SSN with the Indexed algorithm. The ciphertext is randomised, and the client also sends encrypted tokens the server uses to maintain its encrypted index. contentionFactor spreads index entries for the same value to avoid write contention.
  3. The equality query sends an encrypted query token, not ciphertext to compare. The server matches it against __safeContent__ (the per-document tags you can see in the raw document) and returns Liam, without learning the SSN.
  4. QE keeps metadata in two hidden collections, enxcol_.patients.esc and .ecoc. They grow with writes and need compaction over time, part of QE's storage cost.
  5. The server enforces the encryption: a plain insert of an unencrypted SSN fails Document failed validation with reason: 'value was not encrypted', so a buggy client can't silently store plain text.
AdvancedEncryption limits and key management
  • Keys are everything. Lose the master key and the data is gone for good; anyone with the master key and database access can decrypt. Rotate with rewrapManyDataKey(), and keep KMS access tightly scoped.
  • What doesn't work on encrypted fields: sorting, most aggregation operators, and indexes in the normal sense; with CSFLE also range queries. Plan which fields really need it (IDs, account numbers, health data), not whole documents.
  • Not with mongodump: the docs say it isn't supported for encrypted collections, and it never decrypts. Back up the key vault as carefully as the data.
  • Automatic encryption needs the Automatic Encryption Shared Library (crypt_shared); the older mongocryptd process is deprecated in 9.0.
  • In Node, CSFLE and QE need the mongodb-client-encryption package next to the driver (version 6+ for driver 6+). mongosh bundles it, which is why the examples above ran without installing anything.
  • From my notes, the simple version for an interview: "field-level encryption" means the client encrypts sensitive fields, so the database never sees them. The modern answer adds: Queryable Encryption, which can still search them.

#Monitoring and the profiler

Added

A database that's slow but not broken is the usual production problem, and you can't fix what you can't see. MongoDB gives you three layers. The slow query log: every operation slower than slowms (100 ms by default) is written to the server log with its plan and counters. The database profiler: the same information stored as documents in each database's system.profile collection, so you can query it. And live state: $currentOp for what's running now, serverStatus for counters, mongostat and mongotop for a top-like view. Atlas wraps these in its Query Profiler and Performance Advisor.

The mental model: the slow log is a speed camera that photographs only cars over the limit; the profiler is the same camera filing the photos in a searchable database; $currentOp is looking out of the window right now.

The ch22.big collection has 400,000 documents and no index on pad. With the profiler on at level 1 and a 50 ms threshold:

mongosh
db.setProfilingLevel(1, { slowms: 50 }).was
db.big.countDocuments({ pad: /y/ })
db.system.profile.find(
  { ns: "ch22.big" },
  { _id: 0, op: 1, "command.aggregate": 1, "command.pipeline": 1, planSummary: 1, keysExamined: 1, docsExamined: 1, millis: 1 }
).sort({ ts: -1 }).limit(1)
Result
1
0
[
  {
    op: 'command',
    command: {
      aggregate: 'big',
      pipeline: [
        { '$match': { pad: /y/ } },
        { '$group': { _id: 1, n: { '$sum': 1 } } }
      ]
    },
    keysExamined: 0,
    docsExamined: 400000,
    millis: 99,
    planSummary: 'COLLSCAN'
  }
]
What’s happening
  1. setProfilingLevel(1, { slowms: 50 }) turns the profiler on for this database for operations slower than 50 ms. .was shows the previous level, 1, from an earlier run. Level 0 is off (slow operations are still logged), 2 records every operation.
  2. countDocuments({ pad: /y/ }) counts documents whose pad contains a y: none (0), but finding that out means reading all 400,000 documents.
  3. The profiler stored it as one document in system.profile. countDocuments shows up as what it really is, an aggregate with $match and $group.
  4. The numbers that matter: planSummary: 'COLLSCAN' (no index used), keysExamined: 0, docsExamined: 400000 to return a count of 0, millis: 99. A large docsExamined compared with what's returned is the signature of a missing index (see explain() and executionStats).
  5. After db.big.createIndex({ n: 1 }), the query find({ n: 399999 }) used IXSCAN, ran under the threshold, and so was never profiled (countDocuments on system.profile for it returned 0). The profiler shows you the problems, not the healthy queries.
AdvancedThe same query in the slow log

Profiling on or off, anything slower than slowms is logged. The interesting fields of that line (the full line also has the command, locks, queues and more):

Output
{"t": {"$date": "2026-10-16T16:01:42.255+00:00"}, "c": "COMMAND", "msg": "Slow query", "attr": {"type": "command", "ns": "ch22.big", "appName": "mongosh 2.13.0", "planSummary": "COLLSCAN", "keysExamined": 0, "docsExamined": 400000, "nreturned": 0, "queryHash": "7518F44B", "planCacheShapeHash": "7518F44B", "durationMillis": 99}}
AdvancedReading slow log lines well
  • The log is JSON (since 4.4), so you can feed it to any log tool and filter on "msg":"Slow query".
  • appName says which client sent it. Set appName in your app's connection string and slow queries point straight at the service that sent them.
  • queryHash / planCacheShapeHash group all queries of the same shape (same fields and operators, different values), which is how you find "the query that runs 10,000 times a minute at 80 ms" instead of the one-off.
  • On mongos there's no profiler; setProfilingLevel there only changes the slow-log threshold.
  • Changing slowms also changes what's logged. db.getProfilingStatus() on 9.0 also reports sampleRate (log or profile only a fraction of slow operations) and slowinprogms: 5000, a newer threshold for operations that are still in progress.
AdvancedLive operations: $currentOp and killOp

A transaction holds a write lock on product 7 for 15 seconds. A plain update of the same document waits. From a third shell, find the waiting operation and kill it:

mongosh
const op = db.getSiblingDB("admin").aggregate([
  { $currentOp: {} },
  { $match: { ns: "ch22.products", op: "update" } }
]).toArray()[0];
({ opid: op.opid, secs_running: op.secs_running })
db.killOp(op.opid)
Result
{ opid: 1056770, secs_running: Long('4') }
{
  info: 'attempting to kill op',
  ok: 1,
  ...
}

And in the shell whose update was waiting:

Output
Interrupted: Plan executor error during update :: caused by :: operation was interrupted
What’s happening
  1. $currentOp (an aggregation stage on admin) lists every operation running on the server, with its namespace, command, how long it has been running (secs_running) and lock state. Filtering it in the pipeline is the modern form of db.currentOp().
  2. The waiting update has been running for 4 seconds, stuck behind the open transaction's lock (as in the previous chapter's "What a long transaction really blocks").
  3. db.killOp(opid) asks the server to interrupt it: attempting to kill op. The operation checks for interruption regularly and stops.
  4. The client gets Interrupted: … operation was interrupted. Killing an operation is safe for data: a single-document write is either done or not done, and a killed transaction is aborted.
  5. In real incidents, filter $currentOp on secs_running: { $gt: 10 } or waitingForLock: true to find the culprits, and kill the blocker (here, the long transaction) rather than its victims.
AdvancedServer-wide counters: serverStatus, mongostat, mongotop

A handful of the hundreds of serverStatus fields tell you most of what you need:

mongosh
const s = db.serverStatus();
({ uptimeSecs: s.uptime, connections: { current: s.connections.current, available: s.connections.available },
   opcounters: s.opcounters, cacheBytes: s.wiredTiger.cache["bytes currently in the cache"],
   cacheMax: s.wiredTiger.cache["maximum bytes configured"] })
Result
{
  uptimeSecs: 2503,
  connections: { current: 20, available: 8172 },
  opcounters: {
    insert: Long('401338'),
    query: Long('321'),
    update: Long('5068'),
    delete: Long('59'),
    getmore: Long('17948'),
    command: Long('39013'),
    aggregate: Long('7685')
  },
  cacheBytes: 316780806,
  cacheMax: Long('7871660032')
}

With a loop of updates and reads running against product 7, mongostat (one line per second) and mongotop (time spent per collection) showed:

Output
insert query update delete getmore command dirty used flushes vsize  res qrw arw net_in net_out conn  set repl                time
    *0   410    410     *0     819  2455|0  0.1% 4.0%       0 5.18G 524M 0|0 0|0  3.26m   1.95m   24 rs22  SLV Oct 16 16:04:30.686
    *0   443    442     *0     885  2649|0  0.1% 4.0%       0 5.18G 524M 0|0 0|0  3.52m   2.09m   24 rs22  SLV Oct 16 16:04:31.684

                     ns    total    read    write    2026-10-16T16:04:34Z
          ch22.products     50ms    16ms     34ms
         local.oplog.rs     30ms    30ms      0ms
What’s happening
  1. serverStatus counters are cumulative since startup. opcounters (400,000+ inserts from loading ch22.big) tells you the workload mix; sample twice and subtract to get rates. Monitoring tools do exactly that.
  2. connections.current against available is the first thing to check when apps report timeouts: 20 open here. A climbing count usually means an app creating a client per request (see The official driver).
  3. The WiredTiger cache is about 7.9 GB here (by default 50% of RAM minus 1 GB) and holds 317 MB. When the working set no longer fits, reads go to disk and latency jumps.
  4. mongostat shows the per-second rates: about 410 queries and 410 updates a second from the test loop, plus the getmores of the secondaries tailing the oplog. Its repl column printed SLV even though rs.status() reported this member as PRIMARY right afterwards; trust rs.status() for roles.
  5. mongotop shows where the time goes: ch22.products (the test loop) and local.oplog.rs (secondaries reading the oplog).
AdvancedQuery shapes with $queryStats

$queryStats aggregates statistics per query shape. It began as an Atlas feature; on this MongoDB 9.0 Community server it ran, sampling about 1% of operations by default:

mongosh
db.getSiblingDB("admin").aggregate([
  { $queryStats: {} },
  { $limit: 1 },
  { $project: { "key.queryShape.cmdNs": 1, "metrics.execCount": 1, "metrics.totalExecMicros.sum": 1 } }
]).toArray()
Result
[
  {
    key: { queryShape: { cmdNs: { db: 'ch22', coll: 'products' } } },
    metrics: { execCount: Long('37'), totalExecMicros: { sum: Long('2823') } }
  }
]

Each result is one shape with its execution count and total time, so sorting by metrics.totalExecMicros.sum gives "what costs the most overall", often a fast query that runs constantly rather than the slowest one. In 9.0 it also covers insert, update and delete commands.

SQL and MySQL written fresh; MongoDB built from Rohit’s “mongoDB interview” doc. Every query was run on MySQL 9.7, MongoDB 9.0, Redis 8.10 or PostgreSQL 18.

RohitDownloads · All chapters

Sync progress across devices

Type the same private phrase on your Mac and your phone, and your Learned ticks follow you between them — on every study site.

The phrase never leaves this device: only a fingerprint of it is sent, and the server stores a fingerprint of that. Anyone who knows the phrase could see or change your ticks, so pick something you don’t use elsewhere.

Sync is on in this browser.

Sync ID

This ID must be the same on every device. If another device shows a different one, its phrase is different (capital letters count): tap “Turn off here” on it and type the phrase again exactly.