Cloud Explainers

How a NoSQL Database Like MongoDB Works

Databases & SQL11 min videoPublished Nov 5, 2026

How MongoDB works, for beginners, on a real three-server MongoDB replica set: a bookshop saves its books as documents (no table to design first), finds them by fields inside fields, turns a 200,000-document collection scan into a one-document index scan, sees WiredTiger store the data compressed on disk, and then we kill the primary server and watch the replica set elect a new one, with no data lost.

How a NoSQL Database Like MongoDB Works, the full picture: diagram of Bookshop app, mongo1, mongo2, mongo3, WiredTiger, Replica set rs0
The whole picture, from the video's recap.

MongoDB

MongoDB is called a NoSQL database: no tables, no rows. So what does it actually do with your data? This bookshop's database is empty, and nobody created a table. One command saves a book, Dune, with its author and tags inside it. Ask again, and a books collection exists. Later, we'll crash the server holding this book, and still read it.

laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> show collections
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.insertOne({title: "Dune", author: {name: "Frank Herbert", born: 1920}, year: 1965, tags: ["sci-fi", "classic"], price: 9.99, stock: 3}){
  acknowledged: true,
  insertedId: ObjectId('6aec58f23459ec0d6b631cf5')
}
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> show collectionsbooks

Here's the picture we'll build. The bookshop app talks to a MongoDB server, a program called mongod. It keeps the data in memory, and on disk, through its storage engine. And two more servers keep full copies, so one can fail. That's why the prompt says rs0, primary: a replica set, which we'll meet later.

Documents

Let's read the book back. find one returns a document: fields and values, written like JSON, the text format apps use to send data. The author is a smaller document, inside this one. And tags is a list.

laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.findOne({title: "Dune"}){
  _id: ObjectId('6aec58f23459ec0d6b631cf5'),
  title: 'Dune',
  author: { name: 'Frank Herbert', born: 1920 },
  year: 1965,
  tags: [ 'sci-fi', 'classic' ],
  price: 9.99,
  stock: 3
}

Compare that with a table in a relational database. A row has one value per column, so the author and the tags would go in tables of their own, joined back together when you read. A document keeps the whole book in one piece, the way the app already thinks about it. One read brings it all back.

One book, two ways to store it: comparison table of Table rows vs A document, Shape, Author, tags, Reading it, Fits

On the server, it isn't stored as text. MongoDB saves documents in BSON, short for binary JSON. This one takes 161 bytes. BSON also knows types JSON doesn't, like dates and exact integers. And one document can be at most 16 megabytes: plenty for a book.

How MongoDB stores a document: cards of BSON, 161 bytes, Real types, 16 MB at most

Collections

Now a second book, The Hobbit, with no tags, but a field Dune doesn't have: signed. MongoDB takes it as it is. List both, where ones pick the fields to show, and each keeps its own shape.

laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.insertOne({title: "The Hobbit", author: {name: "J.R.R. Tolkien"}, year: 1937, stock: 5, signed: true}){
  acknowledged: true,
  insertedId: ObjectId('6aec58f48c8ec8a7dd0610a9')
}
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.find({}, {_id: 0, title: 1, year: 1, stock: 1, signed: 1})[
  { title: 'Dune', year: 1965, stock: 3 },
  { title: 'The Hobbit', year: 1937, stock: 5, signed: true }
]

So the layers are simple. A MongoDB server holds databases, like bookshop. A database holds collections, like books. And a collection holds documents. Nobody has to describe the fields first. If you want rules, you can add a validator later, but it's your choice, not a requirement.

What a MongoDB server holds: diagram of mongod, bookshop, books, Dune, The Hobbit

The ID

Every document needs an ID, in a field called underscore ID. We never gave Dune one, so the shell made an ObjectId for it. It also knows when it was made: get timestamp reads 10:22:42 out of it, in UTC.

laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.findOne({title: "Dune"})._idObjectId('6aec58f23459ec0d6b631cf5')
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.findOne({title: "Dune"})._id.getTimestamp()ISODate('2026-11-05T10:22:42.000Z')

Here's how. An ObjectId is twelve bytes, written as 24 hex characters. The first four bytes are the time, in seconds. The next five are random, picked once per running program. The last three are a counter that goes up by one. So two programs can make IDs at the same moment, without asking each other, and never clash.

Dune's ObjectId: diagram of 6aec58f23459ec0d6b631cf5, 6aec58f2 = Time it was made, 3459ec0d6b = Random, 631cf5 = Counter, Unique without asking anyone: no central number dispenser

Queries

Queries are documents too. To find books by an author, give the path inside the document, author dot name. Matching a list looks for any item in it: tags sci-fi finds Dune. Operators start with a dollar sign: less than asks for a range, books from before 1950.

laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.find({"author.name": "Frank Herbert"}, {_id: 0, title: 1})[ { title: 'Dune' } ]
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.find({tags: "sci-fi"}, {_id: 0, title: 1})[ { title: 'Dune' } ]
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.find({year: {$lt: 1950}}, {_id: 0, title: 1, year: 1})[ { title: 'The Hobbit', year: 1937 } ]

Read that first filter piece by piece. Author is a field of the book. Dot name steps inside it, to the author's own name field. And Frank Herbert is the value it must equal. No join, and no second table: the server looks inside each book.

A filter on a field inside a field: diagram of {"author.name": "Frank Herbert"}, author = A field of the book, .name = A field inside it, Frank Herbert = The value to match, No join: the server looks inside each document

Changing a document works the same way. We sell one copy of Dune: update one finds it, and inc, short for increment, adds minus one to stock. Only that field changes. Dune had three, and now has two.

laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.updateOne({title: "Dune"}, {$inc: {stock: -1}}){
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.findOne({title: "Dune"}, {_id: 0, title: 1, stock: 1}){ title: 'Dune', stock: 2 }

Here's what happened. The app sends the filter and the change to the server. The server finds the one document that matches, changes it in place, and answers with counts: one matched, one modified. A single document is always changed all or nothing, even with several fields.

One update, one document: diagram of Bookshop, mongod, Dune, updateOne: Dune, stock 3 → 2, matched 1, modified 1, One document's changes: all or nothing

Indexes

Two books are easy. So we loaded a catalogue of 200,000 made-up books, each with its own ISBN code. Find one by its ISBN, and ask MongoDB to explain how it did it. The plan was a collection scan: it read every one of the 200,000 documents, to return one, in 63 milliseconds.

laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.catalog.countDocuments()200000
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.catalog.find({isbn: "978-1000864192"}).explain("executionStats").executionStats{
  executionSuccess: true,
  nReturned: 1,
  executionTimeMillis: 63,
  totalKeysExamined: 0,
  totalDocsExamined: 200000,
  executionStages: {
    isCached: false,
    stage: 'COLLSCAN',
    filter: { isbn: { '$eq': '978-1000864192' } },
    nReturned: 1,
    executionTimeMillisEstimate: 51,
    works: 200001,
    advanced: 1,
    needTime: 199999,
    needYield: 0,
    saveState: 3,
    restoreState: 3,
    isEOF: 1,
    nss: 'bookshop.catalog',
    direction: 'forward',
    docsExamined: 200000
  }
}

So what is an index? It's a sorted list of one field's values, each pointing at its document, like the index at the back of a book. MongoDB keeps it in a B-tree, a tree that stays sorted, so it can jump to one ISBN in a few steps, then fetch just that document. Without it, the only way is to read them all.

Finding one ISBN: diagram of find(isbn), isbn_1 index, One document, catalog

Now create an index on the ISBN field, the one means sorted in ascending order, and run the very same query. This time it's an index scan. It looked at 1 index entry, read 1 document, and took 1 millisecond.

laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.catalog.createIndex({isbn: 1})isbn_1
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.catalog.find({isbn: "978-1000864192"}).explain("executionStats").executionStats{
  executionSuccess: true,
  nReturned: 1,
  executionTimeMillis: 1,
  totalKeysExamined: 1,
  totalDocsExamined: 1,
  executionStages: {
    isCached: false,
    stage: 'FETCH',
    nReturned: 1,
    executionTimeMillisEstimate: 1,
    works: 2,
    advanced: 1,
    needTime: 0,
    needYield: 0,
    saveState: 0,
    restoreState: 0,
    isEOF: 1,
    nss: 'bookshop.catalog',
    docsExamined: 1,
    alreadyHasObj: 0,
    inputStage: {
      stage: 'IXSCAN',
      nReturned: 1,
      executionTimeMillisEstimate: 1,
      works: 2,
      advanced: 1,
      needTime: 0,
      needYield: 0,
      saveState: 0,
      restoreState: 0,
      isEOF: 1,
      nss: 'bookshop.catalog',
      keyPattern: { isbn: 1 },
      indexName: 'isbn_1',
      isMultiKey: false,
      multiKeyPaths: { isbn: [] },
      isUnique: false,
      isSparse: false,
      isPartial: false,
      indexVersion: 2,
      direction: 'forward',
      indexBounds: { isbn: [ '["978-1000864192", "978-1000864192"]' ] },
      keysExamined: 1,
      seeks: 1,
      dupsTested: 0,
      dupsDropped: 0,
      peakTrackedMemBytes: 0
    }
  }
}

That's 200,000 documents read, against one. On our small lab server, 63 milliseconds became 1.

Documents read to find one book: chart of No index: collection scan: 200000 read, ISBN index: index scan: 1 read, 63 ms → 1 ms on our lab server

Indexes aren't free. Every insert and update must change each index too, and they take memory and disk. Every collection gets one on underscore ID by itself. Add others for the fields your app searches on, and check with explain.

Indexes: worth it, not free: cards of Fast lookups, Slower writes, One comes built in, Check, don't guess

On disk

So where do the bytes go? Inside mongod, a storage engine called WiredTiger does that work. Recent data lives in its cache, in memory. Every write is also logged in a journal on disk, so after a crash, the server can replay what it missed. And about every 60 seconds, a checkpoint writes the data files themselves.

Inside one MongoDB server: diagram of Query engine, WiredTiger cache, Journal, Data files

The server confirms it: WiredTiger. Data size counts the catalogue's documents in bytes: about 20.3 megabytes, but on disk the collection takes 5.2. WiredTiger compresses its files, here to about a quarter of the size.

laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.serverStatus().storageEngine.namewiredTiger
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.catalog.dataSize()20288890
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.catalog.storageSize()5226496

Replica sets

One server is one place to fail. So real MongoDB runs as a replica set: here, three servers called rs0. One is the primary, and every write goes to it. The other two are secondaries: they copy each change the primary makes, in the same order.

This status line lists each server's name and role: mongo1 is primary, mongo2 and mongo3 are secondaries. Now a third book, Neuromancer, with write concern majority. That means: don't say yes until most of the three servers have it.

laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> rs.status().members.map(m => m.name + "  " + m.stateStr)[
  'mongo1:27017  PRIMARY',
  'mongo2:27017  SECONDARY',
  'mongo3:27017  SECONDARY'
]
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.insertOne({title: "Neuromancer", author: {name: "William Gibson"}, year: 1984, stock: 2}, {writeConcern: {w: "majority"}}){
  acknowledged: true,
  insertedId: ObjectId('6aec5901aa237189759865bb')
}

Step by step: the primary saves the book, and records the change in its oplog, the operations log. Each secondary reads that log and applies the same change. When one secondary confirms, two of three servers have it, a majority, and only then does the app hear yes. Since version 5.0, majority is MongoDB's default.

A write with write concern majority: diagram of Bookshop, mongo1, mongo2, insert Neuromancer, oplog entry, I have it, acknowledged: true, w: majority is the default since MongoDB 5.0

Here's that entry, read straight from mongo2, a secondary. The op field says i, for insert. ns names the database and collection. And o is the document itself, the same Neuromancer, with a timestamp to keep every change in order.

laptop: mongosh, connected to mongo2 only
rs0 [direct: secondary] bookshop> db.getSiblingDB("local").oplog.rs.find({ns: "bookshop.books"}).sort({$natural: -1}).limit(1)[
  {
    lsid: {
      id: UUID('36e698f2-3b25-4f7c-9115-f93101e972bb'),
      uid: Binary.createFromBase64('47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=', 0)
    },
    txnNumber: Long('1'),
    op: 'i',
    ns: 'bookshop.books',
    ui: UUID('91b55009-fc6b-485e-9d0e-b142e0165a88'),
    o: {
      _id: ObjectId('6aec5901aa237189759865bb'),
      title: 'Neuromancer',
      author: { name: 'William Gibson' },
      year: 1984,
      stock: 2
    },
    o2: { _id: ObjectId('6aec5901aa237189759865bb') },
    stmtId: 0,
    ts: Timestamp({ t: 1793874177, i: 1 }),
    t: Long('1'),
    v: Long('2'),
    wall: ISODate('2026-11-05T10:22:57.327Z'),
    prevOpTime: { ts: Timestamp({ t: 0, i: 0 }), t: Long('-1') }
  }
]

A server crashes

Now the test from the start. On the lab machine, docker kill stops mongo1 at once, like pulling its power cord. The primary is gone, with no chance to say goodbye.

lab host: the three servers run in Docker
docker kill mongo1mongo1

The servers check on each other every two seconds. When the secondaries hear nothing from the primary for ten seconds, one of them calls an election. It needs votes from a majority, two of three, and mongo3 votes for it. mongo2 becomes the new primary, and the app's connection finds it by itself.

Electing a new primary: diagram of Bookshop app, mongo1, mongo2, mongo3, Replica set rs0

In our lab, the whole thing took 10.1 seconds, from the kill to a new primary taking writes. Most of that is the ten seconds of waiting, so a slow network isn't mistaken for a dead server.

From a killed primary to a new one: figure of 10.1 s measured in our lab, source: timed in the demo: docker kill to a new primary, MongoDB 8.3.13

Back on the laptop, the status now shows mongo1 not reachable, and mongo2 as primary. Neuromancer is still there, because a majority had it before the crash. And the app can keep selling: the update works.

laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> rs.status().members.map(m => m.name + "  " + m.stateStr)[
  'mongo1:27017  (not reachable/healthy)',
  'mongo2:27017  PRIMARY',
  'mongo3:27017  SECONDARY'
]
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.findOne({title: "Neuromancer"}, {_id: 0, title: 1, year: 1}){ title: 'Neuromancer', year: 1984 }
laptop: mongosh, connected to the bookshop database
rs0 [primary] bookshop> db.books.updateOne({title: "Neuromancer"}, {$inc: {stock: -1}}){
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}

Why did majority matter? Suppose the app had only waited for mongo1. mongo1 says yes, then dies before any secondary copies the write. mongo2 is elected without it. When mongo1 comes back, it has to roll that write back, and a sale the customer was already told about is gone. With majority, a yes means the write survives an election.

Without majority: a write that disappears: diagram of Bookshop, mongo1, mongo2, sell a book, yes, copy the change, new primary, roll back, w: majority: a yes survives an election

Then we start mongo1 again. It doesn't take its old job back.

lab host: the three servers run in Docker
docker start mongo1mongo1

The status shows it rejoined as a secondary. It caught up by copying the changes it missed. Reading straight from the old primary, Neuromancer shows the sale it missed.

laptop: mongosh
rs0 [primary] bookshop> rs.status().members.map(m => m.name + "  " + m.stateStr)[
  'mongo1:27017  SECONDARY',
  'mongo2:27017  PRIMARY',
  'mongo3:27017  SECONDARY'
]
laptop: mongosh
rs0 [direct: secondary] bookshop> db.books.findOne({title: "Neuromancer"}, {_id: 0, title: 1, stock: 1}){ title: 'Neuromancer', stock: 1 }

On the map, the roles moved, but nothing was lost. mongo2 now takes the writes, and mongo1 and mongo3 copy them. The app never changed its connection: it lists all three servers, and always finds the primary.

After the crash: same data, new roles: diagram of Bookshop app, mongo2, mongo1, mongo3, WiredTiger, Replica set rs0

Sharding

A replica set copies all the data to every server. When a collection grows too big for one, MongoDB can split it across several replica sets, called shards, by a field you choose, the shard key. A router called mongos sends each query to the shard that holds it. We didn't build shards in our lab: most apps never need them.

Sharding: one collection, split up: diagram of Bookshop app, mongos, Shard A, Shard B, Shard C, Shards: replica sets

When it fits

So when does a document database fit? When your data is shaped like objects, like a book with its author and tags, read and written as a whole. When different items need different fields. But think twice when your data is mostly links that must all agree, like orders and payments. A relational database shines in that case.

A document database fits when…: cards of Data shaped like objects, Read as a whole, Shapes that differ, Think twice

Recap

So: MongoDB stores documents, in BSON, inside collections, with no table to design first. Each one gets an ObjectId. Queries reach inside documents, and indexes keep them fast. WiredTiger journals every write and compresses the files. And a replica set keeps copies, so when the primary dies, another takes over in seconds.

If this made MongoDB clearer, subscribing helps us build more real database labs like this one.

Every command and output in this video came from a real run, on MongoDB 8.3.13. Thanks for watching Cloud Explainers.

Sources

Demos: a three-server MongoDB replica set and a laptop in Docker on our own lab machine; the bookshop, its stock and its catalogue are made up. MongoDB is a trademark of MongoDB, Inc.; this channel isn't affiliated with it. Watch the full video on YouTube.

More videos

Python11 min

How to Fix Python pip error: externally-managed-environment

Databases & SQL9 min

How to Fix MySQL Too Many Connections Database Errors

Go (Golang)9 min

Golang Concurrency Will Finally Make Sense After This

AWS10 min

How Amazon S3 Works

Kubernetes10 min

How Kubernetes Works

Docker10 min

How to Fix Docker Port Already Allocated Errors

PostgreSQL10 min

How PostgreSQL Works

Git8 min

How to Fix Git Merge Conflicts

Linux9 min

How to Fix No Space Left on Device in Linux

Networking10 min

How the Internet Works

Databases & SQL9 min

SQL vs NoSQL: When Each One Wins

Auth (OAuth, JWT)8 min

How to Fix JWT Token Expired Errors (401 Unauthorized)

Claude & the Claude Developer Platform9 min

Claude API vs Claude Pro: Why Your Subscription Doesn't Include It

AI agent skills9 min

Why AI Agent Skills Beat Giant Prompts

Linux10 min

Linux Cron Jobs Will Finally Make Sense After This

Cybersecurity10 min

Security Logs Will Finally Make Sense After This

Cybersecurity8 min

How to Fix a Security Leak: An API Key Pushed to GitHub

DNS9 min

DNS Will Finally Make Sense After This

Cybersecurity9 min

Never Ignore a Security Update

Auth (OAuth, JWT)9 min

Two-Factor Authentication Will Finally Make Sense

Networking8 min

Never Expose a Database to the Internet

Cybersecurity10 min

SSH Keys vs Passwords: Which Is Safer for Security?

Linux10 min

The 5 Levels of Linux Boot: From Power On to Login

Networking12 min

The 5 Levels of Network Firewalls

Cybersecurity10 min

Password Security: Why Hackers Crack Weak Passwords

Linux9 min

Why Your Linux Server Runs Out of Memory

Networking10 min

HTTP vs HTTPS: What Actually Changes on Your Network

Linux12 min

Every Linux File Permission You Need to Know

Linux10 min

Linux Processes vs Threads: When Each One Wins

Networking11 min

The OSI Model Will Finally Make Sense After This

Git11 min

The 7 Levels of Git

Networking10 min

Every Network Troubleshooting Command You Need

Cybersecurity10 min

Hashing vs Encryption in Cybersecurity: When Each One Wins

Networking11 min

Why the Internet Ran Out of IP Addresses

Linux11 min

Linux Package Managers: apt vs Snap vs Flatpak

Python9 min

Why Python Is Slow (and When It Doesn't Matter)

Networking11 min

TCP vs UDP: When Each One Wins

Linux8 min

Stop Running Linux as Root (Do This Instead)

Linux11 min

Linux Users and Groups Will Finally Make Sense

Cybersecurity9 min

Why Phishing Beats Good Security (and How to Spot It)

Docker12 min

The 7 Levels of Docker

AI agents & MCP12 min

AI Agents Will Finally Make Sense After This

Git8 min

Stop Using git push --force (Use This Instead)

AI agent skills10 min

Agent Skills vs MCP: When Each One Wins

Networking12 min

Networking Will Finally Make Sense After This

Linux12 min

Every Linux Command You Actually Need

Cybersecurity12 min

The 7 Levels of Cybersecurity

System design12 min

Every System Design Concept You Need, in One Diagram

PostgreSQL10 min

Why PostgreSQL Ignores Your Index