MongoDB Delete Documents

Remove data safely from MongoDB with deleteOne and deleteMany, return what you deleted with findOneAndDelete, drop collections and databases, expire old documents automatically with TTL indexes, and keep history with soft deletes.

Lesson 7 of 43Beginner18 min read

Deleting data is the simplest operation in MongoDB, and the most dangerous. deleteOne removes the first document matching a filter and deleteMany removes all of them. There's no "undo" and no confirmation prompt: a mistyped or empty filter can wipe out a whole collection in milliseconds.

Because of that, professional teams treat deletes carefully. They check a filter with find or countDocuments before deleting, restrict who is allowed to delete, keep backups, and often don't truly delete business data at all. A soft delete marks a document as deleted with a field such as deletedAt, so it can be restored and still appears in reports. Data that genuinely should disappear after a while, such as sessions or password reset tokens, can be removed automatically by a TTL index.

In this lesson you'll delete single and multiple documents, read the result, take a job from a queue with findOneAndDelete, drop collections, set up automatic expiry, and implement soft deletes.

deleteOne and deleteMany

MongoDB Shell
db.sessions.deleteOne({ _id: 'abc123' })
// { acknowledged: true, deletedCount: 1 }

db.logs.deleteMany({ level: 'debug', createdAt: { $lt: ISODate('2026-01-01') } })
// { acknowledged: true, deletedCount: 1834 }
  • The filter works exactly like a find filter, with all the same operators.
  • deletedCount reports how many documents were removed. Zero means nothing matched, which isn't an error.
  • deleteMany({}) removes every document in the collection but keeps the collection and its indexes.

findOneAndDelete, drop and dropDatabase

  • findOneAndDelete(filter, { sort }) removes one document and returns it. It's perfect for simple job queues: each worker takes and removes the oldest job atomically, so two workers never get the same job.
  • db.collection.drop() removes the whole collection, including its indexes, and returns true.
  • db.dropDatabase() removes the current database and everything in it.

On production systems, give application users permission to read and write documents but not to drop collections or databases. The security lesson shows how.

Automatic expiry and soft deletes

A TTL index (time to live) deletes documents automatically once a date field is older than a set number of seconds:

MongoDB Shell
db.sessions.createIndex({ lastSeen: 1 }, { expireAfterSeconds: 3600 })

A background task removes expired documents about once a minute, so expiry isn't exact to the second.

A soft delete keeps the document but marks it:

MongoDB Shell
db.posts.updateOne({ _id: 7 }, { $set: { deletedAt: new Date() } })
db.posts.find({ deletedAt: null })        // normal queries skip deleted posts

Soft deletes make "undo" and audit trails easy, at the cost of remembering to filter deleted documents out everywhere.

Expiring documents at a set time

A TTL index normally counts from a date field, such as "delete sessions an hour after lastSeen". When each document should expire at its own moment, such as a password reset link valid for 15 minutes or a promotion that ends on a given day, store the exact expiry time and set expireAfterSeconds to 0:

MongoDB Shell
db.resetTokens.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 })
db.resetTokens.insertOne({ userId: 42, expireAt: new Date(Date.now() + 15 * 60 * 1000) })

Each document is removed once its expireAt time has passed. A few rules decide whether a TTL index does anything at all:

  • The indexed field must hold a Date (or an array of dates). A document where the field is a string such as '2020-01-01', or where it's missing, never expires.
  • A TTL index must be on a single field; trying to add expireAfterSeconds to a compound index is an error.
  • The background task runs every 60 seconds, and a busy server may take longer, so expired documents can stay visible briefly.

Because expiry isn't instant, queries that must never return expired data should also filter on the date, for example { expireAt: { $gt: new Date() } }. The TTL index then acts as the clean-up crew rather than the gatekeeper.

MongoDB CRUD Syntax

Delete syntax

db.c.deleteOne({ _id: 1 })
db.c.deleteMany({ status: 'expired' })
db.c.countDocuments({ status: 'expired' })      // check first
db.c.findOneAndDelete({ status: 'queued' }, { sort: { createdAt: 1 } })
db.c.deleteMany({})                              // every document
db.c.drop()
db.dropDatabase()
db.c.createIndex({ createdAt: 1 }, { expireAfterSeconds: 86400 })
db.c.updateOne({ _id: 1 }, { $set: { deletedAt: new Date() } })

MongoDB CRUD Summary

  • deleteOne removes the first match; deleteMany removes all matches; deletedCount reports how many.
  • deleteMany({}) empties a collection; drop() removes it completely with its indexes.
  • findOneAndDelete removes and returns a document, which suits job queues.
  • TTL indexes delete documents automatically after a time limit.
  • Soft deletes mark documents with a field such as deletedAt instead of removing them.

MongoDB CRUD Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Deleting one and many

MongoDB Shell
db.logs.insertMany([
  { _id: 1, level: 'debug', msg: 'Cache hit' },
  { _id: 2, level: 'error', msg: 'Payment failed' },
  { _id: 3, level: 'debug', msg: 'Cache miss' },
  { _id: 4, level: 'info', msg: 'User signed in' },
  { _id: 5, level: 'debug', msg: 'Query took 3ms' }
])
db.logs.countDocuments({ level: 'debug' })
db.logs.deleteMany({ level: 'debug' })
db.logs.deleteOne({ _id: 4 })
db.logs.deleteOne({ _id: 99 })
db.logs.find({}, { _id: 0 })
Output
{
  acknowledged: true,
  insertedIds: { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5 }
}
3
{ acknowledged: true, deletedCount: 3 }
{ acknowledged: true, deletedCount: 1 }
{ acknowledged: true, deletedCount: 0 }
[ { level: 'error', msg: 'Payment failed' } ]
Line-by-line explanation
  1. Line 8Counting first confirms the filter matches what you expect.
  2. Line 9deleteMany removes all three debug entries.
  3. Line 10deleteOne removes a single document by _id.
  4. Line 11No document has _id 99, so nothing is deleted; that isn't an error.
Example 2

A job queue with findOneAndDelete

MongoDB Shell
db.jobs.insertMany([
  { _id: 'j1', task: 'send-email', createdAt: ISODate('2026-03-01T09:00:00Z') },
  { _id: 'j2', task: 'resize-image', createdAt: ISODate('2026-03-01T08:00:00Z') },
  { _id: 'j3', task: 'send-email', createdAt: ISODate('2026-03-01T10:00:00Z') }
])

let job
while ((job = db.jobs.findOneAndDelete({}, { sort: { createdAt: 1 } }))) {
  print(`Processing ${job._id}: ${job.task}`)
}
db.jobs.countDocuments()
Output
{
  acknowledged: true,
  insertedIds: { '0': 'j1', '1': 'j2', '2': 'j3' }
}
Processing j2: resize-image
Processing j1: send-email
Processing j3: send-email
0
Line-by-line explanation
  1. Line 8findOneAndDelete takes the oldest job and removes it in one atomic step.
  2. Line 8When the queue is empty it returns null, which ends the loop.
  3. Line 9The returned document is the job that was removed, ready to process.
  4. Line 11Every job has been taken, so none remain.
Example 3

Soft deletes and restoring

MongoDB Shell
db.posts.insertMany([
  { _id: 1, title: 'Hello world', deletedAt: null },
  { _id: 2, title: 'Spring sale', deletedAt: null },
  { _id: 3, title: 'Old news', deletedAt: null }
])
db.posts.updateOne({ _id: 3 }, { $set: { deletedAt: ISODate('2026-03-02T10:00:00Z') } })
db.posts.find({ deletedAt: null }, { title: 1, _id: 0 })
db.posts.countDocuments({ deletedAt: { $ne: null } })
db.posts.updateOne({ _id: 3 }, { $set: { deletedAt: null } })
db.posts.countDocuments({ deletedAt: null })
Output
{ acknowledged: true, insertedIds: { '0': 1, '1': 2, '2': 3 } }
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
[ { title: 'Hello world' }, { title: 'Spring sale' } ]
1
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
3
Line-by-line explanation
  1. Line 6Instead of deleting, the post is marked with the time it was deleted.
  2. Line 7Normal queries only show posts that haven't been deleted.
  3. Line 8Deleted posts are still there for audits and reports.
  4. Line 9Restoring is just clearing the field.

Common MongoDB CRUD Mistakes

An empty or wrong filter

Don't
db.users.deleteMany({ statsu: 'banned' })    // typo: matches nothing, or with {} matches everything
Do
db.users.countDocuments({ status: 'banned' })   // check the number first
db.users.deleteMany({ status: 'banned' })

deleteMany({}) when you meant drop

Don't
db.tempImport.deleteMany({})                  // slow on huge collections, and the indexes remain
Do
db.tempImport.drop()                          // removes the collection in one quick operation

Cleaning up expired data by hand

Don't
// a nightly script that deletes old sessions, which someone forgets to schedule
Do
db.sessions.createIndex({ lastSeen: 1 }, { expireAfterSeconds: 1800 })

MongoDB CRUD in Real Projects

Deleting data in practice

Applications use TTL indexes for sessions, verification codes and temporary uploads. Business records such as orders, invoices and user accounts are usually soft-deleted or archived, because accounting and legal rules require keeping them. When users ask for their personal data to be erased, teams run carefully reviewed scripts that delete or anonymise it across every collection, with backups and logs of what was removed.

MongoDB CRUD FAQ

How do I delete a document by its _id in MongoDB?

Use deleteOne with an _id filter: db.users.deleteOne({ _id: ObjectId('65f1c2a0e4b0a1b2c3d4e5f6') }). If the id is an ObjectId, you must wrap the string in ObjectId(...), otherwise nothing matches and deletedCount is 0. Because _id is always indexed, this is fast on any size of collection.

Is drop faster than deleteMany({}) for emptying a collection?

Yes. drop() removes the collection's data and indexes in one step, while deleteMany({}) deletes documents one by one and updates every index as it goes. Use drop() when you want everything gone and can recreate indexes, and deleteMany({}) when you need to keep the collection, its indexes and validation rules.

Can I undo a delete in MongoDB?

No. There's no recycle bin, so a deleted document is gone unless you can restore it from a backup or an export. That's why careful teams count before deleting, run risky deletes against a copy first, or use soft deletes for data users might want back.

How do I remove a field instead of the whole document?

Use an update with $unset, not a delete: db.users.updateMany({}, { $unset: { tempToken: '' } }). The value you give $unset doesn't matter; the field is removed from every matching document. deleteOne and deleteMany always remove whole documents.

Why is my TTL index not deleting documents?

The most common cause is that the field isn't a real Date, for example a date stored as a string or a number. Also check that the index has expireAfterSeconds set (getIndexes() shows it) and that you've waited at least a minute, since the clean-up task runs every 60 seconds. TTL deletion doesn't run on secondary members directly; they receive the deletes from the primary.

MongoDB CRUD Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the command that removes every document with status "expired".

db.coupons.({ status: 'expired' })
Multiple choiceeasy

What does deleteOne report when nothing matches?

Multiple choicemedium

What does deleteMany({}) do?

Multiple choicemedium

Why does findOneAndDelete suit a job queue?

Multiple choicehard

What does a TTL index with expireAfterSeconds: 3600 on lastSeen do?

MongoDB CRUD Quiz

5 questions to check what you learned.

  1. Which method removes a whole collection?

    • deleteMany({})
    • remove()
    • clear()
    • drop()
  2. What is a soft delete?

    • Deleting slowly
    • Deleting from a backup
    • Marking a document as deleted, for example with deletedAt, instead of removing it
    • Deleting with a TTL index
  3. What's a good habit before running deleteMany?

    • Run the same filter with countDocuments or find to check what will be removed
    • Restart the server
    • Drop the indexes
    • Turn off authentication
  4. Which kinds of data suit TTL indexes?

    • Orders and invoices
    • User accounts
    • Sessions, verification codes and temporary data
    • Product catalogues
  5. Can you undo a deleteMany in MongoDB?

    • Yes, with undo()
    • Yes, within one minute
    • Only in Atlas
    • No, only by restoring from a backup